Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ The `video_generate` tool plays the generated clip inline:

Only logged-in providers appear in the session model picker; the lists above refresh on login/logout. Vision-capable models declare `['text', 'image']` input modalities, and image content is translated to each provider's wire format.

Logged-in cards also show **subscription usage** — per rate-limit window (5-hour session, weekly, and per-model weekly where the plan has one) with the used percentage, a progress bar, and the reset time, plus a Refresh button. Codex usage comes from `chatgpt.com/backend-api/wham/usage` (also reports the plan), Claude usage from `api.anthropic.com/api/oauth/usage`, and Grok usage from the Grok Build CLI proxy's `cli-chat-proxy.grok.com/v1/billing` (the source of the CLI's `/usage` panel; reports the shared weekly pool and the subscription tier). Copilot exposes no usage endpoint, so its card shows no usage section.
Logged-in cards also show **subscription usage** — per rate-limit window (5-hour session, weekly, and per-model weekly where the plan has one) with the used percentage, a progress bar, and the reset time, plus a Refresh button. Codex usage comes from `chatgpt.com/backend-api/wham/usage` (also reports the plan), Claude usage from `api.anthropic.com/api/oauth/usage` (the plan comes from the stored subscription type, since that payload names no tier), and Grok usage from the Grok Build CLI proxy's `cli-chat-proxy.grok.com/v1/billing` (the source of the CLI's `/usage` panel; reports the shared weekly pool and the subscription tier). Copilot exposes no usage endpoint, so its card shows no usage section.

Claude usage additionally rides a **meter in the composer**, left of the model selector and beside the shell's own context meter: a ring showing the limit that applies to the session's current model — a model with its own weekly limit shows that one, everything else the shared weekly pool — which turns amber at 75% and red at 95%. Hovering names the window and revalidates; clicking opens every reported limit with its reset time. It renders only while a Claude model is selected, so it never reports a limit the next turn will not spend.

The meter is deliberately frugal with that endpoint, which is aggressively rate limited and shared with the settings page above: one cache serves every open session, an idle session issues no request at all, a running one asks every 3 minutes, and a refusal never clears the last good reading. A `Retry-After` on a 429 is honoured when the provider sends one.

Also included, registered when the matching provider is enabled:

Expand Down
25 changes: 23 additions & 2 deletions src/auth/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { PROVIDER_IDS, type ProviderId } from './store.js'
import type { ProviderUsage } from '../providers/common.js'
import { OAuthEndpointError, type ProviderUsage } from '../providers/common.js'
import type { ProxyConfigView, ProxyDraft, ProxyInput, ProxyTestResult } from '../http.js'

/** The RPC channel this plugin registers on the host connection. */
Expand Down Expand Up @@ -137,7 +137,28 @@ function failure(error: unknown): RpcResult<unknown> {
// The issues array is zod-shaped upstream; this channel validates by hand.
return { ok: false, error: { code: 'bad-request', message, details: { issues: [] } } }
}
return { ok: false, error: { code: 'internal', message, details: {} } }
return { ok: false, error: { code: 'internal', message: withRetryAfter(error, message), details: {} } }
}

/**
* Append the provider's retry delay to a failure message when it sent one.
*
* The delay belongs in structured data, but the `internal` RpcResult branch
* types `details` as an empty object upstream (`z.object({})` in
* dsh-host-apiproxy's rpc schema), so a field added here would be stripped in
* validation rather than reaching the page. The message is the only channel
* that survives, so the suffix is a stable, parseable one — a browser caller
* that backs off on a 429 can honour the interval the provider actually asked
* for instead of inventing its own.
*
* @param error - the thrown failure, which may carry a parsed `Retry-After`.
* @param message - the failure message as it stands.
* @returns the message, with ` (retry-after: <seconds>s)` appended when known.
*/
function withRetryAfter(error: unknown, message: string): string {
if (!(error instanceof OAuthEndpointError) || error.retryAfterMs === undefined) return message
const seconds = Math.max(1, Math.round(error.retryAfterMs / 1000))
return `${message} (retry-after: ${String(seconds)}s)`
}

function readProvider(payload: unknown): ProviderId {
Expand Down
24 changes: 18 additions & 6 deletions src/client/SubscriptionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,17 @@ const styles: Record<string, CSSProperties> = {
display: 'flex', justifyContent: 'space-between', gap: 8,
fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
},
// Meter geometry follows the shell's own ContextMeter bar (4px, fully
// rounded, filled track, no border) so a progress bar contributed by a
// plugin reads as the same control the product draws elsewhere.
usageTrack: {
height: 6, borderRadius: 3, overflow: 'hidden',
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
height: 4, borderRadius: 999, overflow: 'hidden',
background: 'var(--dsw-alias-interactive-bg-hover)',
},
usageFill: {
height: '100%', borderRadius: 999,
transition: 'width .3s ease, background-color .3s ease',
},
usageFill: { height: '100%', borderRadius: 3 },
manual: { marginTop: 4, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
manualRow: { display: 'flex', gap: 8, marginTop: 6 },
manualInput: {
Expand Down Expand Up @@ -288,11 +294,17 @@ function usageWindowLabel(t: SubscriptionsSectionInjected['t'], window: UsageWin
return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base
}

/** Bar fill color: success normally, warn from 80%, error from 95%. */
/**
* Bar fill color. The steps follow the Claude Code app, which warns from 75%
* rather than 80%, so a plan this panel and that app both report reaches its
* warning shade at the same point. Below the warning step the meter is the
* neutral business tone rather than success green: a half-consumed limit is a
* reading, not an achievement, and green reads as the latter.
*/
function usageBarColor(usedPercent: number): string {
if (usedPercent >= 95) return 'var(--dsw-alias-state-error-primary)'
if (usedPercent >= 80) return 'var(--dsw-alias-state-warn-label)'
return 'var(--dsw-alias-state-success-primary)'
if (usedPercent >= 75) return 'var(--dsw-alias-state-warn-label)'
return 'var(--dsw-static-blue-450)'
}

/** One-line status text of the proxy config card. */
Expand Down
Loading