Skip to content
Merged
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
68 changes: 60 additions & 8 deletions .github/workflows/live.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
name: live canary

# The real-boundary lanes: pack the artifact, install it into a throwaway
# profile next to the published base/web-app bundles, boot a REAL DeepSeek
# Harness from a fresh checkout, and drive the page in a real browser —
# integration (module served) and functional (a capability write persists to
# settings.yaml). Too heavy for the push pipeline, so this lane canaries once
# a week (plus on manual dispatch); the hermetic `ci` workflow stays the fast
# gate.
# The real-boundary lanes, run once per published harness channel: pack the
# artifact, install it into a throwaway profile next to the published
# base/web-app bundles, boot a REAL DeepSeek Harness from the git tag the
# channel's npm dist-tag currently names, and drive the page in a real
# browser — integration (module served) and functional (a capability write
# persists to settings.yaml). Resolving the dist-tag each run makes this a
# radar: when upstream publishes a new rc or alpha, THIS lane is what tests
# us against it first, and a red run opens one tracking issue instead of
# anyone discovering the break by hand. Daily, plus on manual dispatch; the
# hermetic `ci` workflow stays the fast gate.

on:
schedule:
- cron: '17 21 * * 1'
- cron: '17 21 * * *'
workflow_dispatch:

permissions:
contents: read
# The escalation step opens one issue per red channel instead of letting a
# silent cron turn stale.
issues: write

jobs:
live:
runs-on: ubuntu-latest
timeout-minutes: 90
# One red channel must not cancel the other: each is a verdict on its own.
strategy:
fail-fast: false
matrix:
# `tag` is the npm dist-tag naming that channel's current public build.
channel: [{ key: rc, label: rc }, { key: alpha, label: alpha }]
env:
BMP_DSH_DIR: ${{ github.workspace }}/dsh
# Chrome is preinstalled on the runner; puppeteer-core just needs the path.
Expand All @@ -34,29 +46,69 @@ jobs:
cache: npm
- run: npm ci

# Channel → harness git tag, resolved fresh: the dist-tag names the
# published version, and upstream tags every release `dsh-v<version>`.
# A channel whose dist-tag vanished (a closed line) skips cleanly —
# loud failure is reserved for a line that exists but broke us.
- name: resolve channel tag
id: resolve
run: |
v=$(npm view @deepseek-ai/dsh "dist-tags.${{ matrix.channel.key }}" 2>/dev/null || true)
if [ -z "$v" ]; then
echo "::warning::dist-tag ${{ matrix.channel.key }} is absent; channel skipped"
else
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "ref=dsh-v$v" >> "$GITHUB_OUTPUT"
fi

# The harness is consumed as a source checkout: liveBoot runs the real
# CLI through tsx out of it. Its lockfile pins the wire line the plugin
# is tested against; a harness regression fails this lane, not a PR.
- uses: actions/checkout@v4
if: steps.resolve.outputs.ref
with:
repository: deepseek-ai/deepseek-harness
path: dsh
ref: ${{ steps.resolve.outputs.ref }}
- uses: pnpm/action-setup@v4
if: steps.resolve.outputs.ref
with:
# The action would otherwise read OUR package.json (no version →
# "No pnpm version is specified"); point it at the harness's one,
# exactly like the harness's own CI resolves it.
package_json_file: dsh/package.json
- uses: actions/setup-node@v4
if: steps.resolve.outputs.ref
with:
node-version: 22
- run: pnpm install
if: steps.resolve.outputs.ref
working-directory: dsh
# The checkout's loader entries import workspace sources through
# node_modules junctions — lib/ exists only after the harness builds
# itself (local checkouts quietly carry those artifacts; CI's does not).
- run: pnpm build
if: steps.resolve.outputs.ref
working-directory: dsh

- run: npm run test:live
if: steps.resolve.outputs.ref
- run: npm run test:functional
if: steps.resolve.outputs.ref

# Escalate a red run into ONE open issue per channel per harness version:
# never a daily duplicate, never a silent canary.
- name: escalate on failure
if: failure() && steps.resolve.outputs.ref
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
title="live canary red on ${{ matrix.channel.label }} (${{ steps.resolve.outputs.version }})"
run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
existing=$(gh issue list --search "\"$title\" in:title" --state open --json number --jq '.[0].number')
if [ -z "$existing" ]; then
gh issue create --title "$title" \
--body "Harness ${{ matrix.channel.label }} ${{ steps.resolve.outputs.version }} breaks the live lanes. CI run: $run_url"
else
gh issue comment "$existing" --body "Still red: $run_url"
fi
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ Notable changes to better-model-provider. Versions track the published git
tags (npm artifact when it ships); the verification matrix each release was
held to lives in `CONTRIBUTING.md`.

## [0.0.4] - 2026-08-31

- **dsh 0.1.2-alpha.2 support.** The alpha folded owner failures into a
shared `RemoteError` class and rebadged the hyphenated wire codes into
slash namespaces — `settings-conflict` → `settings/conflict`,
`settings-rejected` → `settings/rejected`, `model-discovery-failed` →
`llm/model-discovery-rejected` — with every details payload unchanged.
The adapter now folds the rename back at the failure arm, so the CAS
conflict keeps its localized, actionable message; the copy is fieldwise
because the alpha's real `RemoteError` instance carries `message` as an
Error-inherited non-enumerable a spread would silently drop. All four
face methods are untouched; both live lanes pass against the alpha.2
checkout (and 0.1.2-alpha.1 and the published rc line).

- **The CI canary is now a two-channel radar.** The weekly live lane
becomes a daily matrix over both published harness channels (rc and
alpha), each resolved from its npm dist-tag to the matching
`dsh-v<version>` git tag, so an upstream publish is tested against us
on the next run — and a red channel opens ONE tracking issue instead of
waiting to be noticed by hand.

## [0.0.3] - 2026-08-29

- **dsh 0.1.2-alpha.1 (source master) support, without dropping the npm line.**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Dedicated-adapter apps (built-in DeepSeek / OpenAI Codex) declare their capabili

## Compatibility

We declare compatibility with the whole **dsh 0.1.x line**: contract `@deepseek-ai/dsh-api-remotes >=0.1.0-rc.7 <0.2.0`, real-harness lanes verified on rc.7, rc.8, 0.1.1-rc.2, and the 0.1.2-alpha.1 source master. Surfaces outside the contract degrade silently. Development gates, live lanes, and invariants: see [CONTRIBUTING.md](CONTRIBUTING.md).
We declare compatibility with the whole **dsh 0.1.x line**: contract `@deepseek-ai/dsh-api-remotes >=0.1.0-rc.7 <0.2.0`, real-harness lanes verified on rc.7, rc.8, 0.1.1-rc.2, and 0.1.2-alpha.1/alpha.2 — a daily CI canary re-proves both published channels (rc and alpha) against their current npm dist-tags. Surfaces outside the contract degrade silently. Development gates, live lanes, and invariants: see [CONTRIBUTING.md](CONTRIBUTING.md).

## License

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ CI 在 main 每次全绿后自动重建并发布成品到 `master` 分支,此

## 兼容性

我们声明兼容**整个 dsh 0.1.x 系**:契约 `@deepseek-ai/dsh-api-remotes >=0.1.0-rc.7 <0.2.0`,live 车道已在 rc.7、rc.8、0.1.1-rc.2 及 0.1.2-alpha.1 源树 master 上实证。契约之外的能力面静默降级。开发门禁、live 车道与不变式见 [CONTRIBUTING.md](CONTRIBUTING.md)。
我们声明兼容**整个 dsh 0.1.x 系**:契约 `@deepseek-ai/dsh-api-remotes >=0.1.0-rc.7 <0.2.0`,live 车道已在 rc.7、rc.8、0.1.1-rc.2 及 0.1.2-alpha.1 / alpha.2 上实证,并有一条按日运行的 CI 金丝雀对 rc 与 alpha 两个已发布频道按各自 npm dist-tag 的当前版本持续复验。契约之外的能力面静默降级。开发门禁、live 车道与不变式见 [CONTRIBUTING.md](CONTRIBUTING.md)。

## 许可证

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "better-model-provider",
"version": "0.0.3",
"version": "0.0.4",
"description": "Per-model capability declaration for DeepSeek Harness: reasoning-effort levels (with wire spellings) and request modalities (text/image) for OpenAI-compatible providers — one settings section, no YAML hand-editing.",
"license": "MIT",
"repository": {
Expand Down
54 changes: 36 additions & 18 deletions src/client/wire.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* Dual-generation Remote adapter. dsh ≤0.1.1-rc.2 carries the settings/llm
* namespaces on the `connection.api` face; dsh 0.1.2-alpha.1 (the current
* harness master) installs them as traced `remote.<ns>` Cordis services whose
* generated Typert proxies take positional arguments, rename `providers` to
* `listConfigurableProviders`, and resolve the slimmer `RemoteResult`
* envelope (no outer `rpcId`/`result` wrapper, values unboxed from their
* `{providers}`/`{models}` carriers). The page logic keeps speaking ONE face
* — {@link IRemoteApi}, the legacy published contract — so this module is the
* only place in the plugin that knows both generations.
* namespaces on the `connection.api` face; dsh 0.1.2-alpha.1 installs them as
* traced `remote.<ns>` Cordis services whose generated Typert proxies take
* positional arguments, rename `providers` to `listConfigurableProviders`,
* and resolve the slimmer `RemoteResult` envelope (no outer `rpcId`/`result`
* wrapper, values unboxed from their `{providers}`/`{models}` carriers);
* 0.1.2-alpha.2 then wraps owner failures in `RemoteError` and rebadges the
* hyphen codes into slash namespaces. The page logic keeps speaking ONE face
* — {@link IRemoteApi}, the legacy published contract, with those renames
* folded back — so this module is the only place that knows both generations.
*
* The alpha-generation METHOD SIGNATURES are hand-projected here from the
* generated clients in the harness checkout: the npm-published
Expand All @@ -27,10 +28,9 @@ import type {
} from './types.ts'

/**
* Slim Remote failure carried by the alpha.1 generation's error branch. Its
* `code` is an open string upstream (the closed union lives in the carrier
* package, which would invert the dependency edge), but the VALUES are the
* same wire vocabulary the legacy RpcError union closed over.
* Slim Remote failure carried by the alpha generation's error branch: a
* code-discriminated `RemoteError` union instance since alpha.2, wire-cardinal
* `code`/`message`/`details` fields throughout both alphas.
*/
interface AlphaRemoteFailure {
readonly code: string
Expand Down Expand Up @@ -94,20 +94,38 @@ interface AlphaLlmRemote {
*/
const ALPHA_RPC_ID = 'bmp-alpha' as RpcId

/**
* alpha.2 rebadged the hyphen wire codes into slash namespaces
* (`settings-conflict` → `settings/conflict`) with details unchanged; the
* page branches on the legacy union, so the adapter folds the renames back.
*/
const ALPHA_TO_LEGACY_CODES: Readonly<Record<string, RpcError['code']>> = {
'settings/conflict': 'settings-conflict',
}

/** Fold an alpha failure into the legacy RpcError frame: translate the renamed codes, carry message/details. */
function toLegacyError(error: AlphaRemoteFailure): RpcError {
// Fieldwise, never a spread: alpha.2's RemoteError instance has its `message`
// Error-inherited and non-enumerable, so `{...error}` would silently drop it.
return {
code: ALPHA_TO_LEGACY_CODES[error.code] ?? error.code,
message: error.message,
details: error.details,
} as RpcError
}

/**
* Re-wrap the slim alpha envelope as the legacy carrier frame the page's
* `unwrap` reads: stamp the shared `rpcId` echo the alpha no longer
* carries, box the value arm, and forward the failure arm. The single cast
* at the `error` slot pins exactly the documented claim that the alpha's
* open `code` string and the legacy closed RpcError union spell the same
* wire vocabulary — `settings-conflict` keeps its `{ns, expected, actual}`
* details, so the CAS branch in the store stays put.
* carries, box the value arm, and translate the failure arm. The CAS details
* (`{ns, expected, actual}`) survive the code rename untouched, so the
* store's conflict branch stays put.
*/
function toLegacyEnvelope<T>(call: Promise<AlphaRemoteResult<T>>): Promise<RpcResponse<T>> {
return call.then(result =>
result.ok
? { rpcId: ALPHA_RPC_ID, result }
: { rpcId: ALPHA_RPC_ID, result: { ok: false, error: result.error as RpcError } },
: { rpcId: ALPHA_RPC_ID, result: { ok: false, error: toLegacyError(result.error) } },
)
}

Expand Down
25 changes: 25 additions & 0 deletions tests/wire.client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,31 @@ describe('resolveRemoteApiGently — alpha generation (dsh 0.1.2-alpha.1)', () =
expect(caught).toMatchObject({ name: 'HarnessRpcError', code: 'settings-conflict', message })
})

test('an alpha.2 RemoteError-shaped failure folds its slash code back to the legacy spelling', async () => {
// alpha.2 hands a real RemoteError INSTANCE: `message` is Error-inherited
// (non-enumerable, so a spread would drop it) and the code is slash-spelled.
class FakeRemoteError extends Error {
readonly code = 'settings/conflict'
readonly details = { ns: 'llm-pi-ai', expected: 1, actual: 2 }
}
const fake = new FakeRemoteError('expected revision 1, actual 2')
expect(Object.keys(fake)).not.toContain('message')
const { services } = alphaRemotes({ mutate: () => Promise.resolve({ ok: false, error: fake }) })
const resolved = resolveGently(services)

const response = await resolved.settings.mutate({
ns: 'llm-pi-ai',
ops: [{ op: 'set', path: ['providers', 'ksyun', 'models'], value: [] }],
expectedRevision: 1,
})
if (response.result.ok) throw new Error('expected the failure branch')
expect(response.result.error).toEqual({
code: 'settings-conflict',
message: 'expected revision 1, actual 2',
details: { ns: 'llm-pi-ai', expected: 1, actual: 2 },
})
})

test('a carrier-folded failure rides the value-mapping arm untouched', async () => {
// Upstream folds carrier failures into the same error branch
// (transportError's 'internal' catch-all) — the boxing helpers must not
Expand Down
Loading