opencode-deepseek-peak 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { Plugin } from "@opencode/plugin"
2
+ import { DEFAULT_SIGNAL_HEADERS, readApiPricePeriod } from "./api-signal.js"
3
+ import { DeepSeekPeakRpc } from "./rpc.js"
4
+ import { scheduleInfo } from "./schedule.js"
5
+ import type { PeakStatus } from "./types.js"
6
+
7
+ function stringList(value: unknown, fallback: readonly string[]): string[] {
8
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
9
+ ? value
10
+ : [...fallback]
11
+ }
12
+
13
+ export default Plugin.define({
14
+ id: "opencode-deepseek-peak",
15
+ async setup(context) {
16
+ const providerIDs = new Set(stringList(context.options.providerIDs, ["deepseek"]))
17
+ const signalHeaders = stringList(context.options.apiSignalHeaders, DEFAULT_SIGNAL_HEADERS)
18
+ const configuredTtl = Number(context.options.apiSignalTtlMs)
19
+ const apiSignalTtlMs = Number.isFinite(configuredTtl) && configuredTtl >= 0
20
+ ? configuredTtl
21
+ : 5 * 60 * 1000
22
+ const statuses = new Map<string, PeakStatus>()
23
+ const apiSignals = new Map<string, { period: PeakStatus["period"]; evidence: string; observedAt: number }>()
24
+
25
+ const rpc = await context.rpc.register(DeepSeekPeakRpc, {
26
+ status: async (input) => {
27
+ const { sessionID } = input as { sessionID: string }
28
+ const status = statuses.get(sessionID)
29
+ return status ? { found: true, status } : { found: false }
30
+ },
31
+ // Lets the TUI show the current period as soon as a session opens with a
32
+ // DeepSeek model selected, without waiting for the first request.
33
+ preview: async (input) => {
34
+ const { sessionID } = input as { sessionID: string }
35
+ const observed = statuses.get(sessionID)
36
+ if (observed) return { found: true, status: observed }
37
+ try {
38
+ const session = await context.session.get({ sessionID })
39
+ const model = session.model
40
+ if (!model || !isDeepSeek(model.providerID)) return { found: false }
41
+ const schedule = scheduleInfo(new Date())
42
+ const evidence = schedule.holiday
43
+ ? `Chinese public holiday: ${schedule.holiday} (no request observed yet)`
44
+ : "Current schedule (no request observed yet)"
45
+ return {
46
+ found: true,
47
+ status: {
48
+ sessionID,
49
+ providerID: model.providerID,
50
+ modelID: model.id,
51
+ period: schedule.period,
52
+ scheduledPeriod: schedule.period,
53
+ source: "official-schedule",
54
+ phase: "request",
55
+ mismatch: false,
56
+ observedAt: Date.now(),
57
+ evidence,
58
+ holiday: schedule.holiday,
59
+ },
60
+ }
61
+ } catch {
62
+ return { found: false }
63
+ }
64
+ },
65
+ })
66
+
67
+ const publish = async (status: PeakStatus) => {
68
+ statuses.set(status.sessionID, status)
69
+ await rpc.events.emit("updated", status as unknown as Readonly<Record<string, unknown>>)
70
+ }
71
+
72
+ const isDeepSeek = (providerID: string) => providerIDs.has(providerID)
73
+ const modelKey = (providerID: string, modelID: string) => `${providerID}/${modelID}`
74
+
75
+ const stopRequest = await context.session.hook("model.request", async (event) => {
76
+ if (event.kind !== "primary" || !isDeepSeek(event.model.providerID)) return
77
+ const now = Date.now()
78
+ const schedule = scheduleInfo(new Date(now))
79
+ const cached = apiSignals.get(modelKey(event.model.providerID, event.model.id))
80
+ const api = cached && now - cached.observedAt <= apiSignalTtlMs ? cached : undefined
81
+ await publish({
82
+ sessionID: event.sessionID,
83
+ providerID: event.model.providerID,
84
+ modelID: event.model.id,
85
+ period: api?.period ?? schedule.period,
86
+ scheduledPeriod: schedule.period,
87
+ source: api ? "api-header" : "official-schedule",
88
+ phase: "request",
89
+ mismatch: api ? api.period !== schedule.period : false,
90
+ observedAt: now,
91
+ evidence: api
92
+ ? `${api.evidence} (cached)`
93
+ : schedule.holiday
94
+ ? `Chinese public holiday: ${schedule.holiday}`
95
+ : "DeepSeek official UTC pricing schedule",
96
+ holiday: schedule.holiday,
97
+ })
98
+ })
99
+
100
+ const stopResponse = await context.session.hook("http.response", async (event) => {
101
+ if (event.kind !== "primary" || !isDeepSeek(event.model.providerID)) return
102
+ const api = readApiPricePeriod(event.response.headers, signalHeaders)
103
+ if (!api) return
104
+ const now = Date.now()
105
+ const schedule = scheduleInfo(new Date(now))
106
+ apiSignals.set(modelKey(event.model.providerID, event.model.id), {
107
+ ...api,
108
+ observedAt: now,
109
+ })
110
+ await publish({
111
+ sessionID: event.sessionID,
112
+ providerID: event.model.providerID,
113
+ modelID: event.model.id,
114
+ period: api.period,
115
+ scheduledPeriod: schedule.period,
116
+ source: "api-header",
117
+ phase: "response",
118
+ mismatch: api.period !== schedule.period,
119
+ observedAt: now,
120
+ evidence: api.evidence,
121
+ holiday: schedule.holiday,
122
+ })
123
+ })
124
+
125
+ return async () => {
126
+ await stopRequest.dispose()
127
+ await stopResponse.dispose()
128
+ await rpc.dispose()
129
+ }
130
+ },
131
+ })
@@ -0,0 +1,32 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import { createMemo, For, Show } from "solid-js"
4
+ import { themeColors } from "../core/display.js"
5
+ import { createT } from "../i18n.js"
6
+ import type { IndicatorProps } from "./PeakPanel.js"
7
+
8
+ /** Prompt-footer status: peak stands out in the default text color, off-peak stays muted. */
9
+ export function FooterStatus(props: IndicatorProps) {
10
+ const t = createT(() => props.lang())
11
+
12
+ const segments = createMemo(() => {
13
+ const status = props.status()
14
+ if (!status) return []
15
+ const { normal, muted, warning } = themeColors(props.context.theme)
16
+ const peak = status.period === "peak"
17
+ const label = peak ? t("period.peak") : t("period.offPeak")
18
+ const out = [{ text: `DS ${label}`, color: peak ? normal : muted }]
19
+ if (status.source === "api-header") out.push({ text: " API", color: muted })
20
+ if (status.mismatch) out.push({ text: " ⚠", color: warning })
21
+ out.push({ text: " ·", color: muted })
22
+ return out
23
+ })
24
+
25
+ return (
26
+ <Show when={props.enabled() && props.status()}>
27
+ <text>
28
+ <For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
29
+ </text>
30
+ </Show>
31
+ )
32
+ }
@@ -0,0 +1,59 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import type { Plugin } from "@opencode/plugin/tui"
4
+ import { createEffect, createMemo, For, Show } from "solid-js"
5
+ import { evidenceLabel, themeColors } from "../core/display.js"
6
+ import { createT, type LangCode } from "../i18n.js"
7
+ import type { PeakStatus } from "../types.js"
8
+
9
+ export interface IndicatorProps {
10
+ context: Plugin.Context
11
+ status: () => PeakStatus | undefined
12
+ enabled: () => boolean
13
+ lang: () => LangCode
14
+ sessionID: string
15
+ refresh: (sessionID: string) => void
16
+ }
17
+
18
+ export interface PanelProps extends IndicatorProps {
19
+ evidence: () => boolean
20
+ }
21
+
22
+ export function PeakPanel(props: PanelProps) {
23
+ const t = createT(() => props.lang())
24
+
25
+ // The server keeps the last observed request per session, so opening a
26
+ // session must pull that status instead of waiting for the next event.
27
+ createEffect(() => {
28
+ const sessionID = props.sessionID
29
+ if (sessionID) props.refresh(sessionID)
30
+ })
31
+
32
+ // Single compact status line, flush left, plus an optional muted evidence line.
33
+ const segments = createMemo(() => {
34
+ const status = props.status()
35
+ if (!status) return []
36
+ const { normal, muted, warning } = themeColors(props.context.theme)
37
+ const peak = status.period === "peak"
38
+ const out = [
39
+ { text: "DeepSeek ", color: muted },
40
+ { text: `● ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? normal : muted },
41
+ ]
42
+ if (status.source === "api-header") out.push({ text: " · API", color: muted })
43
+ if (status.mismatch) out.push({ text: ` · ${t("label.mismatch")} ⚠`, color: warning })
44
+ return out
45
+ })
46
+
47
+ return (
48
+ <Show when={props.enabled() && props.status()}>
49
+ <box flexDirection="column" alignItems="flex-start" alignSelf="flex-start">
50
+ <text>
51
+ <For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
52
+ </text>
53
+ <Show when={props.evidence() ? props.status() : undefined}>
54
+ {(status) => <text fg={themeColors(props.context.theme).muted}>{evidenceLabel(status(), t)}</text>}
55
+ </Show>
56
+ </box>
57
+ </Show>
58
+ )
59
+ }
package/src/rpc.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { Rpc } from "@opencode/plugin/rpc"
2
+
3
+ const statusSchema = {
4
+ type: "object",
5
+ properties: {
6
+ sessionID: { type: "string" },
7
+ providerID: { type: "string" },
8
+ modelID: { type: "string" },
9
+ period: { type: "string", enum: ["peak", "off-peak"] },
10
+ scheduledPeriod: { type: "string", enum: ["peak", "off-peak"] },
11
+ source: { type: "string", enum: ["official-schedule", "api-header"] },
12
+ phase: { type: "string", enum: ["request", "response"] },
13
+ mismatch: { type: "boolean" },
14
+ observedAt: { type: "number" },
15
+ evidence: { type: "string" },
16
+ holiday: { type: "string" },
17
+ },
18
+ required: [
19
+ "sessionID",
20
+ "providerID",
21
+ "modelID",
22
+ "period",
23
+ "scheduledPeriod",
24
+ "source",
25
+ "phase",
26
+ "mismatch",
27
+ "observedAt",
28
+ "evidence",
29
+ ],
30
+ additionalProperties: false,
31
+ } as const
32
+
33
+ export const DeepSeekPeakRpc = Rpc.define({
34
+ id: "deepseek-peak",
35
+ methods: {
36
+ status: {
37
+ input: {
38
+ type: "object",
39
+ properties: { sessionID: { type: "string" } },
40
+ required: ["sessionID"],
41
+ additionalProperties: false,
42
+ },
43
+ output: {
44
+ type: "object",
45
+ properties: {
46
+ found: { type: "boolean" },
47
+ status: statusSchema,
48
+ },
49
+ required: ["found"],
50
+ additionalProperties: false,
51
+ },
52
+ },
53
+ preview: {
54
+ input: {
55
+ type: "object",
56
+ properties: { sessionID: { type: "string" } },
57
+ required: ["sessionID"],
58
+ additionalProperties: false,
59
+ },
60
+ output: {
61
+ type: "object",
62
+ properties: {
63
+ found: { type: "boolean" },
64
+ status: statusSchema,
65
+ },
66
+ required: ["found"],
67
+ additionalProperties: false,
68
+ },
69
+ },
70
+ },
71
+ events: {
72
+ updated: { schema: statusSchema },
73
+ },
74
+ })
@@ -0,0 +1,66 @@
1
+ import Holidays from "date-holidays"
2
+ import type { PricePeriod } from "./types.js"
3
+
4
+ const chinaHolidays = new Holidays("CN", { languages: ["en"] })
5
+
6
+ function datePartsInChina(date: Date) {
7
+ const parts = new Intl.DateTimeFormat("en-CA", {
8
+ timeZone: "Asia/Shanghai",
9
+ year: "numeric",
10
+ month: "2-digit",
11
+ day: "2-digit",
12
+ }).formatToParts(date)
13
+ const value = (type: Intl.DateTimeFormatPartTypes) =>
14
+ parts.find((part) => part.type === type)?.value ?? ""
15
+ return `${value("year")}-${value("month")}-${value("day")}`
16
+ }
17
+
18
+ /** Name of the Chinese public holiday covering this instant, when any. */
19
+ export function chinesePublicHoliday(date: Date): string | undefined {
20
+ // Peak windows are 09:00–18:00 in China, so their UTC and China calendar
21
+ // dates align. Noon avoids boundary ambiguity in holiday libraries.
22
+ const chinaDate = datePartsInChina(date)
23
+ const holiday = chinaHolidays.isHoliday(new Date(`${chinaDate}T12:00:00+08:00`))
24
+ if (!Array.isArray(holiday)) return undefined
25
+ return holiday.find((entry) => entry.type === "public")?.name
26
+ }
27
+
28
+ export function isChinesePublicHoliday(date: Date): boolean {
29
+ return chinesePublicHoliday(date) !== undefined
30
+ }
31
+
32
+ function peakWindow(date: Date): PricePeriod {
33
+ const hour = date.getUTCHours() + date.getUTCMinutes() / 60
34
+ return (hour >= 1 && hour < 4) || (hour >= 6 && hour < 10) ? "peak" : "off-peak"
35
+ }
36
+
37
+ export interface ScheduleInfo {
38
+ period: PricePeriod
39
+ /** Set when a Chinese public holiday makes the whole day off-peak. */
40
+ holiday?: string
41
+ }
42
+
43
+ /**
44
+ * DeepSeek's official pricing schedule as published in September 2026:
45
+ * 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday, excluding Chinese
46
+ * public holidays. Interval ends are exclusive.
47
+ */
48
+ export function scheduleInfo(
49
+ date: Date,
50
+ holidayName: (value: Date) => string | undefined = chinesePublicHoliday,
51
+ ): ScheduleInfo {
52
+ const day = date.getUTCDay()
53
+ if (day === 0 || day === 6) return { period: "off-peak" }
54
+ const holiday = holidayName(date)
55
+ if (holiday) return { period: "off-peak", holiday }
56
+ return { period: peakWindow(date) }
57
+ }
58
+
59
+ export function officialPricePeriod(
60
+ date: Date,
61
+ holidayCheck: (value: Date) => boolean = isChinesePublicHoliday,
62
+ ): PricePeriod {
63
+ const day = date.getUTCDay()
64
+ if (day === 0 || day === 6 || holidayCheck(date)) return "off-peak"
65
+ return peakWindow(date)
66
+ }
package/src/tui.tsx ADDED
@@ -0,0 +1,112 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import type { Plugin } from "@opencode/plugin/tui"
4
+ import { createSignal } from "solid-js"
5
+ import { INITIAL_SETTINGS, statusLabel } from "./core/display.js"
6
+ import { createT, detectLang, type LangCode } from "./i18n.js"
7
+ import { FooterStatus } from "./panel/FooterStatus.js"
8
+ import { PeakPanel } from "./panel/PeakPanel.js"
9
+ import { DeepSeekPeakRpc } from "./rpc.js"
10
+ import type { PeakStatus, TuiSettings } from "./types.js"
11
+ import { CommandRoot } from "./v2/commands.js"
12
+
13
+ const mod: Plugin.Definition = {
14
+ id: "opencode-deepseek-peak-tui",
15
+ setup(context) {
16
+ const client = context.client.rpc(DeepSeekPeakRpc)
17
+ const [settings, update] = context.storage.store<TuiSettings>(
18
+ "opencode-deepseek-peak.settings",
19
+ { initial: { ...INITIAL_SETTINGS, lang: detectLang() } },
20
+ )
21
+ // Stored settings may predate the `lang` field.
22
+ const lang = () => (settings.lang ?? detectLang()) as LangCode
23
+ const t = createT(lang)
24
+ const [statuses, setStatuses] = createSignal<Record<string, PeakStatus>>({})
25
+ const lastToast = new Map<string, string>()
26
+
27
+ const unsubscribe = client.events.on("updated", (event) => {
28
+ const status = event.data as unknown as PeakStatus
29
+ setStatuses((current) => ({ ...current, [status.sessionID]: status }))
30
+ if (!settings.toast) return
31
+ const fingerprint = `${status.period}:${status.mismatch}`
32
+ if (!settings.toastEveryRequest && lastToast.get(status.sessionID) === fingerprint) return
33
+ lastToast.set(status.sessionID, fingerprint)
34
+ context.ui.toast.show({
35
+ title: t("toast.title"),
36
+ message: statusLabel(status, t),
37
+ variant: status.period === "peak" ? "warning" : "success",
38
+ duration: 4500,
39
+ })
40
+ })
41
+
42
+ const readStatus = async (sessionID: string) => {
43
+ const result = await client.status({ sessionID }) as { found: boolean; status?: PeakStatus }
44
+ if (result.found && result.status) return result.status
45
+ try {
46
+ const preview = await client.preview({ sessionID }) as { found: boolean; status?: PeakStatus }
47
+ return preview.found ? preview.status : undefined
48
+ } catch {
49
+ return undefined
50
+ }
51
+ }
52
+
53
+ // Pull the server's last observed status so the indicators render as soon
54
+ // as a session opens, not only after the next DeepSeek request.
55
+ const refresh = (sessionID: string) => {
56
+ void readStatus(sessionID)
57
+ .then((status) => {
58
+ if (status) setStatuses((current) => ({ ...current, [status.sessionID]: status }))
59
+ })
60
+ .catch(() => {})
61
+ }
62
+
63
+ const unregisterCommands = context.ui.slot({
64
+ append: "app",
65
+ render: () => (
66
+ <CommandRoot context={context} settings={settings} update={update} readStatus={readStatus} lang={lang} />
67
+ ),
68
+ })
69
+ const unregisterSidebar = context.ui.slot({
70
+ append: "sidebar.content",
71
+ render: (props) => {
72
+ const sessionID = String(props.sessionID ?? "")
73
+ return (
74
+ <PeakPanel
75
+ context={context}
76
+ status={() => statuses()[sessionID]}
77
+ enabled={() => settings.sidebarIndicator !== false}
78
+ evidence={() => settings.sidebarEvidence !== false}
79
+ lang={lang}
80
+ sessionID={sessionID}
81
+ refresh={refresh}
82
+ />
83
+ )
84
+ },
85
+ })
86
+ const unregisterFooter = context.ui.slot({
87
+ append: "prompt.footer.status",
88
+ render: (props) => {
89
+ const sessionID = String(props.sessionID ?? "")
90
+ return (
91
+ <FooterStatus
92
+ context={context}
93
+ status={() => statuses()[sessionID]}
94
+ enabled={() => settings.indicator}
95
+ lang={lang}
96
+ sessionID={sessionID}
97
+ refresh={refresh}
98
+ />
99
+ )
100
+ },
101
+ })
102
+
103
+ return () => {
104
+ unsubscribe()
105
+ unregisterCommands()
106
+ unregisterSidebar()
107
+ unregisterFooter()
108
+ }
109
+ },
110
+ }
111
+
112
+ export default mod
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { LangCode } from "./i18n.js"
2
+
3
+ export type PricePeriod = "peak" | "off-peak"
4
+ export type StatusSource = "official-schedule" | "api-header"
5
+ export type StatusPhase = "request" | "response"
6
+
7
+ export interface PeakStatus {
8
+ sessionID: string
9
+ providerID: string
10
+ modelID: string
11
+ period: PricePeriod
12
+ scheduledPeriod: PricePeriod
13
+ source: StatusSource
14
+ phase: StatusPhase
15
+ mismatch: boolean
16
+ observedAt: number
17
+ evidence: string
18
+ /** Chinese public holiday making the whole day off-peak, when applicable. */
19
+ holiday?: string
20
+ }
21
+
22
+ export interface TuiSettings {
23
+ toast: boolean
24
+ indicator: boolean
25
+ sidebarIndicator: boolean
26
+ sidebarEvidence: boolean
27
+ toastEveryRequest: boolean
28
+ lang: LangCode
29
+ }
@@ -0,0 +1,83 @@
1
+ import type { Plugin } from "@opencode/plugin/tui"
2
+ import { statusLabel } from "../core/display.js"
3
+ import { createT, LANG_META, type LangCode } from "../i18n.js"
4
+ import type { PeakStatus, TuiSettings } from "../types.js"
5
+ import { openSettingsMenu } from "./settings-menu.js"
6
+
7
+ interface CommandProps {
8
+ context: Plugin.Context
9
+ settings: TuiSettings
10
+ update: (mutation: (draft: TuiSettings) => void) => Promise<void>
11
+ readStatus: (sessionID: string) => Promise<PeakStatus | undefined>
12
+ lang: () => LangCode
13
+ }
14
+
15
+ /** Command layer (registered in the app slot: slash commands stay available when the sidebar is hidden). */
16
+ export function CommandRoot(props: CommandProps) {
17
+ const { context } = props
18
+ const t = createT(() => props.lang())
19
+
20
+ context.keymap.layer(() => ({
21
+ mode: "global" as const,
22
+ commands: [
23
+ {
24
+ id: "opencode-deepseek-peak.peak.status",
25
+ title: t("command.status.title"),
26
+ description: t("command.status.description"),
27
+ group: "DeepSeek",
28
+ palette: true,
29
+ slash: { name: "deepseek-peak" },
30
+ run: async () => {
31
+ const route = context.ui.router.current()
32
+ if (route.type !== "session") {
33
+ await context.ui.dialog.alert({ title: t("status.title"), message: t("status.noSession") })
34
+ return
35
+ }
36
+ try {
37
+ const status = await props.readStatus(route.sessionID)
38
+ await context.ui.dialog.alert({
39
+ title: t("status.title"),
40
+ message: status
41
+ ? `${statusLabel(status, t)}\n${t("status.source")}: ${status.source}\n${t("status.evidence")}: ${status.evidence}`
42
+ : t("status.noRequest"),
43
+ })
44
+ } catch {
45
+ context.ui.toast.show({ message: t("status.unavailable"), variant: "error" })
46
+ }
47
+ },
48
+ },
49
+ {
50
+ id: "opencode-deepseek-peak.peak.sections",
51
+ title: t("command.sections.title"),
52
+ description: t("command.sections.description"),
53
+ group: "DeepSeek",
54
+ palette: true,
55
+ slash: { name: "deepseek-peak-sections" },
56
+ run: () => {
57
+ openSettingsMenu(context, props.settings, props.update, props.lang)
58
+ },
59
+ },
60
+ {
61
+ id: "opencode-deepseek-peak.peak.lang",
62
+ title: t("command.lang.title"),
63
+ description: t("command.lang.description"),
64
+ group: "DeepSeek",
65
+ palette: true,
66
+ slash: { name: "deepseek-peak-lang" },
67
+ run: async () => {
68
+ const picked = await context.ui.dialog.select<LangCode>({
69
+ title: t("settings.lang"),
70
+ options: LANG_META.map((meta) => ({ title: meta.label, value: meta.code })),
71
+ current: props.lang(),
72
+ })
73
+ if (picked !== undefined) {
74
+ await props.update((draft) => {
75
+ draft.lang = picked
76
+ })
77
+ }
78
+ },
79
+ },
80
+ ],
81
+ }))
82
+ return null
83
+ }