dsh-blackjack plugin

August 27, 2026 · View on GitHub

English | 简体中文

⚠️ Third-party community project disclaimer

This plugin is published and maintained by an individual developer and is not affiliated with any model vendor: it is not an official project of any vendor, does not represent their position, is not endorsed or authorized by them, and does not use their names or logos as branding elements. The dsh- / bj- prefixes in the package name and commands merely follow the community's plugin naming convention, identifying this plugin's compatibility target and namespace; they do not imply it is published or approved by dsh officially.

Install it into your own dsh and open a blackjack table right there in the conversation: win hands to earn the game currency CHIP, convert CHIP one-way into "redeemed credit", and spend that credit only through this plugin's fallback route — and only when the model credit you configured yourself has run out will the plugin use it to catch that one failed request, so you don't have to break off what you're doing to top up or wait.

Participation is always free: a fixed number of free hands every day, losing costs you nothing, and it never touches your own key or balance. All the details are in "Participation rules" below.


What this is and how to play

The table's authoritative state is held by a game server (packages/server, self-hosted by the operator); the plugin is just its client:

  • Text commands: the /blackjack family costs nothing — they all go through dsh's command layer rather than sending a model message, so playing itself consumes none of your token credit.
  • Graphical table (optional): if your dsh profile includes the Web GUI (e.g. @deepseek-ai/dsh-web-app), a clickable table card appears in the conversation stream. Its action buttons are equivalent to the text commands, just a different surface. In a terminal-only profile without the Web GUI the text commands work as usual — the plugin does not error out over a missing optional service.

One round: /blackjack deal to start (uses one of the day's free hands by default, or deal <amount> to raise from your balance) → play it out with the action commands bj-hit / bj-stand as prompted → settle, and the winner takes CHIP.

Command table

CommandWhat it does
/blackjackShow current status: the round if one is in progress, otherwise balance / free hands / redemption threshold
/blackjack agreeDisplay the full terms (third-party disclaimer, non-withdrawable, non-transferable, etc.). Displays only — it is not consent
/blackjack agree confirmConfirm consent after reading the terms and register a table account. Before this step, both dealing and redemption are refused
/blackjack deal [amount]Deal a new round. With no argument it uses one of the day's free hands; with a positive integer it raises that amount from your balance
/blackjack exchange [amount]Convert an eligible balance into spendable credit. The first time (before you have bound GitHub) it gives you a pairing link to complete the binding in a browser; after binding, passing an amount redeems right there in the command with no browser involved
/blackjack reset confirmClear the table credentials stored on this machine (server-side records are unaffected; running agree confirm again registers a new account)
/bj-hitTake another card
/bj-standStand and go to settlement
/bj-doubleDouble the bet and take exactly one more card
/bj-splitSplit a pair into two hands
/bj-insureBuy insurance when the dealer's up card is an ace
/bj-declineDecline insurance

The six bj-* action commands only mean anything while a round is in progress; with no round they tell you to run /blackjack deal first.

The two redemption paths

Redemption requires binding GitHub (one GitHub identity maps to exactly one wallet — the only anti-sockpuppet gate on the prize pool's exit). Binding can only be done in a browser, because it goes through GitHub's official OAuth authorization code flow. But binding only has to happen once:

  1. First time: /blackjack exchange gives you a short-lived pairing code and link. Open it in a browser, click "Continue with GitHub" to bind, then redeem with the form on that same page.
  2. After that: /blackjack exchange <amount>. The plugin calls the server's redemption endpoint directly with the player token it already holds — no pairing code, no browser. With no amount, it first echoes back your balance and the threshold and asks you to add the amount: redemption is irreversible, and the plugin will not guess a number for you.

A balance below the threshold, or an amount larger than your balance, gets you a clear message (spelling out how many CHIP the threshold is, or simply that your balance is short), and the ledger does not move a cent. The language of these messages follows the locale config key — English by default, see "Configuration" below.


Installation

dsh plugin --profile web add -w dsh-blackjack
dsh --profile web

Replace web with whichever profile you are actually installing into. The package is published on npm, so the command above works as-is.

The launch screen used to be blank (fixed in 0.2.2)

From 0.2.2, typing /blackjack straight into dsh's launch screen (the "Into the Unknown" view you get when you open a fresh dsh) brings up the table — no need to send a throwaway message first, and no need to dig an old conversation out of the sidebar.

On 0.2.1 and earlier it does not: the command really did execute, but nothing appeared on screen, which looked exactly like a broken plugin. On an older version, let a conversation genuinely start first (send any message, or open an existing conversation from the sidebar) and then run /blackjack agree. Clicking "New conversation" does not help — that sends you back to the launch screen.

The cause is not in this plugin but in one predicate in the host (dsh-client-runtime@0.1.0-rc.6, lib/client.js:7753):

function hasVisibleConversationContent(chat) {
  return chat.order.some((key) => chat.nodes.get(key)?.kind !== "command");
}

A generic command row is deliberately excluded from "visible conversation content", so the session view considers the session still empty and renders nothing at all. The host's own /goal is unaffected because ui-goal additionally projects a non-command node that activates the conversation. This plugin now takes the same route: it projects the command line you typed as a chat node of its own — just an existing event drawn under a different kind, writing nothing, emitting no new events, and putting nothing into history the model can see.

This is a plugin-side workaround, and the host-level gap remains: a plugin with only a server half and no browser half still has no way to make its commands visible on the launch screen. The upstream discussion (with the line-by-line root cause and this plugin's full approach): https://github.com/deepseek-ai/deepseek-harness/discussions/4066. The minimal plugin originally used to reproduce it (13 lines, no slots and no services) is in this repo at docs/repro/dsh-probe-repro/.

To change the code, or to install a development build that isn't on npm yet, install from source:

git clone https://github.com/yul761/dsh-blackjack
cd dsh-blackjack && pnpm install          # builds the plugin's dist/ automatically
dsh plugin --profile web add -w "$PWD/packages/plugin"

Note that you cannot just run pnpm add github:yul761/dsh-blackjack: this is a monorepo, and installing from the git source gets you the repository root (a private workspace root with no entry point) rather than the plugin package in packages/plugin.

Why -w is mandatory: every dsh profile directory is itself a pnpm workspace (it ships its own pnpm-workspace.yaml), and pnpm refuses by default to add a dependency to the root of a workspace without -w/--workspace-root. Omit the flag and the install command fails outright rather than leaving you in a half-installed state. This is not a trap unique to this plugin — other community dsh plugins' install docs hit the same thing and reached the same conclusion.

Version requirement: this plugin pins dsh 0.1.0-rc.6

The plugin pins every @deepseek-ai/dsh-* peer exactly at 0.1.0-rc.6, not ^0.1.0-rc.6. That is not conservatism, it's necessary: these packages release in lockstep and the rcs are not compatible with each other, and ^ lets the package manager install a newer rc for whichever peer the host doesn't have — that is exactly how 0.1.0 broke (dsh-llm-deepseek@rc.8 against the host's dsh-llm@rc.6, failing at startup with does not provide an export named 'offloadRequestImages').

So: please use dsh@0.1.0-rc.6. On a different rc you'll see peer version warnings, and the fix is to wait for the plugin to catch up with a new release.

After a successful install, dsh reads the dsh.bundle.patch declaration in the plugin's package.json and wires blackjack.cordis.yml into the profile's bundle layer automatically — you don't have to hand-write a --patch overlay.

Configuration

It works out of the box: serverUrl points by default at the instance self-hosted by this project's operator, so you can play without changing anything. You can override it in your profile's cordis.patch.yml:

KeyTypeDefaultDescription
serverUrlstringhttps://server-production-493c.up.railway.appThe game server's address. To run your own prize pool, change this to your own deployment — the server is open source in the same repo as this plugin; see the deployment section of the repo root README
enabledbooleantrueSet to false to keep it installed but completely inert: no commands registered, no routes and no fallback hooks attached
locale'en' | 'zh''en'The language of replies from the commands (/blackjack, /bj-*, …). The graphical table ignores this key — it follows dsh's own Settings → General → Language automatically, so players using the GUI never have to touch this. Only terminal-only profiles (no Web GUI) need to set it once here, because the Node side cannot read the host's language setting
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: blackjack
  config:
    serverUrl: https://your-blackjack-server.example.com

Exactly what triggers the fallback route (please read all of it)

The plugin attaches an observer to llm/stream, but it never intercepts a working call. The fallback only kicks in when all of the following hold at once:

  1. the call used a model provider you configured yourself (not the plugin's own pool route — this avoids an infinite loop);

  2. the call produced no content at all — even a single character already emitted disqualifies it; the plugin will never switch sources after content has started coming back, since that could only produce duplication or truncation;

  3. the call ended in a quota-exhausted class of error (not a network error, not a bad parameter, not anything else);

  4. you have already completed /blackjack agree confirm in this plugin (table credentials exist on this machine; without them the plugin does not even try).

    This checks only for credentials and does not pre-check whether your redeemed credit is sufficient. That is deliberate: checking the balance would add a network request on top of every failed call and would still return a possibly-stale snapshot, while the "has credentials, zero balance" path is safe by construction — the pool call is rejected by the server outright, and the plugin then hands you back your original error unchanged, exactly as if the plugin weren't installed.

Only when all four hold does the plugin resend that failed call to the pool route.

Which model the pool uses (important): for a request resent to the pool, the operator picks the model server-side, and it is not necessarily the one you selected in the UI. This is deliberate — the pool spends the operator's own upstream balance, so whoever pays picks the model; it also avoids the "you picked a model the server doesn't recognize, so the fallback wasted a catch" class of failure. So for a call completed by the pool, the model shown in the UI may differ from the model that actually served it. The current operator instance uses deepseek-v4-flash.

At most 4 concurrent pool calls at a time. The server sets the per-player concurrency limit to 4: the turn in which dsh creates a session fires 3 LLM calls at once, and with the gate at 1 the other two were rejected outright, so the fallback failed on exactly the first message where it should have worked (measured on a real machine on 2026-08-22 and raised from 1 to 4 — the 3 measured plus one slot of headroom). Going over the limit gets you a clear "too many requests" (429). Note also that a pool request first freezes an estimated hold, and a balance that has just crossed the redemption threshold only fits one, so under real concurrency the later calls may be rejected for insufficient credit — that comes back as a clear message stating how much was needed and how much is left, not a vague failure.

Where the switch notice went (please note): on a successful switch the plugin emits a line saying it has switched to pool credit to continue this request, but in the current dsh version it only goes into dsh's own log and does not appear in the conversation UI — this version gives plugins no channel for inserting a non-model message into a conversation, and the one available channel would write the content into history visible to the model, which this project explicitly refuses to do (no context pollution, nothing extra for the model to see). In other words: a call may have been completed with pool credit while you see no notice in the UI. To confirm the consumption, check how your redeemed credit changed with /blackjack. Once dsh offers plugins an in-session notification slot, this will become an in-UI notice.

If the pool is also out, or the pool service itself is unreachable, the plugin does not fabricate a successful result — you get back your call's original failure reason unchanged, exactly as if this plugin weren't installed. Put differently: with this plugin installed, the outcome of a call can only stay the same or get better, never worse.


Participation rules

  • Participation is always free. A fixed number of free hands every day, at zero cost. There is no design — and there will never be one — that lets you pay for hands, pay for better odds, or pay for better payouts.
  • CHIP and redeemed credit are one-way and closed.
    • not withdrawable, not convertible to any fiat currency, and no monetary amount is displayed in the UI;
    • not transferable (including between players) and non-refundable;
    • never land in any model vendor's account;
    • the only use is spending them through the game server's fallback route — there is no other exit.
  • Inactive balances are recycled. Accounts with no games and no consumption for 7 consecutive days have their balance recycled into the prize pool.
  • Redeemed credit does not currently expire. Early copy said "valid until the end of the season", but the server's season settlement does not touch redeemed credit — that sentence did not match the implementation, so it has been changed to describe reality. Whether to zero it out at season end has not been decided; if it ever is, it will be announced first.
  • Your own API key never enters this system: the plugin does not read, host or hold any key for your own provider, and the fallback route uses credentials issued by the game server itself.

Uninstalling and clearing local credentials

To clear only the table login credentials stored locally (say you're switching machines, or want to start over with a fresh account), you don't have to uninstall the plugin:

/blackjack reset confirm

(Without confirm it first echoes back an explanation of what the operation does, to prevent mistakes.) This clears only the credentials stored on this machine; the account record on the server is unaffected, and /blackjack agree confirm registers a new account.

To remove the plugin entirely:

dsh plugin --profile web remove dsh-blackjack