@wxip/dsh-sub2api 0.2.2

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Load-time invariant checks for dsh-sub2api.
3
+ *
4
+ * The host half declares `inject: ['llm', 'settings', 'credentials']`; the
5
+ * loader surfaces a missing provider as a waiting row, so a dedicated
6
+ * invariant would only duplicate that signal. This module exists to keep the
7
+ * package's export surface stable (lib/invariant.js) and to host any future
8
+ * structural checks.
9
+ *
10
+ * @module dsh-sub2api/invariant
11
+ */
12
+ export declare function invariant(condition: unknown, message: string): asserts condition;
13
+ export default invariant;
@@ -0,0 +1,9 @@
1
+
2
+ //#region src/invariant.ts
3
+ function invariant(condition, message) {
4
+ if (!condition) throw new Error(`dsh-sub2api: ${message}`);
5
+ }
6
+ var invariant_default = invariant;
7
+
8
+ //#endregion
9
+ export { invariant_default as default, invariant };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Best-effort defense guard for pi-ai's prefix-token estimation.
3
+ *
4
+ * Background: pi-ai's `AssistantMessage.usage` is required in its types and
5
+ * `estimateContextTokens` dereferences `usage.totalTokens` on that contract.
6
+ * The harness path is already safe — `dsh-llm-pi-ai` attaches a zero `Usage`
7
+ * (`emptyPiUsage()`) to every reconstructed assistant message. The guard here
8
+ * only defends against *other* callers that build pi-ai contexts without
9
+ * `usage` (hand-rolled clients, future adapters), which would otherwise die
10
+ * with a bare `Cannot read properties of undefined (reading 'totalTokens')`
11
+ * deep inside estimation.
12
+ *
13
+ * This plugin cannot control the pi-ai version through npm — Node resolves
14
+ * pi-ai from the dsh install, not from this package. So the guard is applied
15
+ * as a precise idempotent edit to the bundled `estimate.js`:
16
+ *
17
+ * ```js
18
+ * assistant.stopReason !== "error" &&
19
+ * assistant.usage !== undefined &&
20
+ * calculateContextTokens(assistant.usage) > 0
21
+ * ```
22
+ *
23
+ * It runs at plugin apply time — before any pi-ai request (pi-ai's API modules
24
+ * are lazy-loaded, so `estimate.js` is only imported on the first stream). A
25
+ * refusal to write (read-only install) only logs a warning; the harness path
26
+ * works without the guard, and an upstream pi-ai guard makes this a no-op.
27
+ *
28
+ * @module dsh-sub2api/pi-ai-patch
29
+ */
30
+ /** Outcome of one patch attempt. */
31
+ export type PiAiPatchResult = {
32
+ kind: 'patched';
33
+ file: string;
34
+ } | {
35
+ kind: 'already';
36
+ file: string;
37
+ } | {
38
+ kind: 'skipped';
39
+ reason: string;
40
+ };
41
+ /**
42
+ * Apply the multi-turn guard to the dsh-bundled pi-ai `estimate.js` when it is
43
+ * missing. Idempotent; only a byte-exact edit is ever made.
44
+ */
45
+ export declare function applyPiAiMultiTurnPatch(): PiAiPatchResult;
package/lib/pi-ai.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * dsh-sub2api → dsh-llm-pi-ai profile bridge.
3
+ *
4
+ * The LLM routes this plugin used to own (`sub2api-openai` / `sub2api-claude`
5
+ * / `sub2api-grok`) are now served by the harness's pi-ai
6
+ * adapter (`dsh-llm-pi-ai`, mounted dormant by dsh-base): protocol
7
+ * serialization, streaming, usage mapping, replay, and retry handling all live
8
+ * in pi-ai. This module is the translation layer — it turns this plugin's
9
+ * `llm-sub2api:` settings section (gateway baseURL + per-group model catalogs
10
+ * + keys) into `llm-pi-ai:` provider profiles and writes them through the
11
+ * settings service, so routes register the moment the section lands and drop
12
+ * again when a key is cleared.
13
+ *
14
+ * Every sub2api group is translated as a *hand-declared* route — pi-ai ships
15
+ * no provider under these keys — with `api` naming the group's native wire
16
+ * protocol (openai→responses, claude→messages, grok→chat-completions),
17
+ * `baseURL` set to the shared gateway, and `models` carrying the configured
18
+ * catalog with each model's capacity, modalities, and reasoning levels mapped
19
+ * onto pi-ai's vocabulary (`none` becomes `off` with wire spelling `none`).
20
+ *
21
+ * @module dsh-sub2api/pi-ai
22
+ */
23
+ import type { Context } from '@deepseek-ai/cordis';
24
+ import type { PiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai';
25
+ import type { Config } from './index.ts';
26
+ /./index.jstings namespace owned by dsh-llm-pi-ai. */
27
+ export declare const PI_AI_NS = "llm-pi-ai";
28
+ /** Route prefix this plugin's groups own in the llm-pi-ai profile dict. */
29
+ export declare const ROUTE_PREFIX: string;
30
+ export type { PiAiModelProfile, PiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai';
31
+ /** The llm-pi-ai settings section value this plugin writes. */
32
+ export interface PiAiSettingsSection {
33
+ providers?: Record<string, PiAiProviderProfile>;
34
+ }
35
+ /**
36
+ * Build the `llm-pi-ai` provider profile dict for every configured sub2api
37
+ * group. A group is emitted only when it has both a key and at least one
38
+ * model — a hand-declared pi-ai route needs a non-empty `models` list, and a
39
+ * keyless group would otherwise surface as an unauthenticated route.
40
+ */
41
+ export declare function translateToPiAi(config: Config): Record<string, PiAiProviderProfile>;
42
+ /**
43
+ * Write the translated profiles into the `llm-pi-ai` settings section. Routes
44
+ * under this plugin's `sub2api-` prefix are replaced wholesale; any other
45
+ * route the user configured (e.g. through the built-in Models page) is
46
+ * preserved. The write goes through the settings service, so dsh-llm-pi-ai's
47
+ * own validation (schema + `assertServiceable`) refuses an unserviceable
48
+ * profile at the write site and the section keeps its last good value.
49
+ */
50
+ export declare function syncPiAiProfiles(ctx: Context, config: Config): Promise<void>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * HTTP routes for the dsh-sub2api settings page.
3
+ *
4
+ * The browser half of a static plugin talks to the host through the web
5
+ * server (there is no package-private `host.call` seam outside dynamic
6
+ * packages), so this module exposes read/write endpoints for the plugin's
7
+ * settings section. Requests are restricted to trusted local origins — the
8
+ * same `trustedRequest` posture the oauth plugin uses — because these routes
9
+ * mutate configuration and echo credential state.
10
+ *
11
+ * @module dsh-sub2api/routes
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import { type CatalogModel, type Config, type ImageToolsConfig, type ProviderProfile } from './in./index.jsxport declare const ROUTES: {
15
+ readonly get: "/plugins/dsh-sub2api/config";
16
+ readonly set: "/plugins/dsh-sub2api/config";
17
+ readonly discover: "/plugins/dsh-sub2api/discover";
18
+ readonly usage: "/plugins/dsh-sub2api/usage";
19
+ readonly status: "/plugins/dsh-sub2api/status";
20
+ readonly attachment: "/plugins/dsh-sub2api/attachment";
21
+ };
22
+ export interface ConfigPayload {
23
+ baseURL: string;
24
+ catalogFormat: 'structured-v1';
25
+ providers: Record<string, {
26
+ keyConfigured: boolean;
27
+ models: CatalogModel[];
28
+ }>;
29
+ tools: ImageToolsConfig;
30
+ }
31
+ interface RouteContext {
32
+ config: () => Config;
33
+ setConfig: (config: Config) => void | Promise<void>;
34
+ listRegisteredRoutes: () => string[];
35
+ /**
36
+ * Resolve the stored credential for one provider route. Used by discovery
37
+ * and usage probes when the settings form does not carry a freshly typed
38
+ * key (keys are write-only and stay in the credential store).
39
+ */
40
+ resolveApiKey: (route: string, profile: ProviderProfile) => Promise<string>;
41
+ }
42
+ export declare function registerRoutes(ctx: Context, routes: RouteContext): void;
43
+ export {};
package/package.json ADDED
@@ -0,0 +1,160 @@
1
+ {
2
+ "name": "@wxip/dsh-sub2api",
3
+ "description": "Connect your sub2api gateway to DeepSeek Harness: OpenAI-compatible multi-provider routes (OpenAI / Claude / Grok) behind one base URL, with per-key model discovery, usage lookup, global image generation, and a settings page.",
4
+ "version": "0.2.2",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "sub2api",
10
+ "llm",
11
+ "openai",
12
+ "claude",
13
+ "grok",
14
+ "image-generation"
15
+ ],
16
+ "homepage": "https://github.com/GodD6366/dsh-sub2api#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/GodD6366/dsh-sub2api/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/GodD6366/dsh-sub2api.git"
23
+ },
24
+ "type": "module",
25
+ "main": "lib/index.js",
26
+ "types": "lib/index.d.ts",
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "engines": {
32
+ "node": "^22.19.0 || >=24.0.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown && node scripts/wrap-client.mjs",
36
+ "prepare": "node scripts/prepare.mjs",
37
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
38
+ "check": "npm run typecheck && npm run build",
39
+ "patch:pi-ai": "node scripts/patch-pi-ai.mjs",
40
+ "test": "npm run build && node --test tests/*.test.mjs",
41
+ "release:npm": "npm run typecheck && npm test && npm publish --ignore-scripts --access public --registry=https://registry.npmjs.org/"
42
+ },
43
+ "exports": {
44
+ ".": {
45
+ "types": "./lib/index.d.ts",
46
+ "default": "./lib/index.js"
47
+ },
48
+ "./client": "./lib/client.js",
49
+ "./cordis.patch.yml": "./cordis.patch.yml",
50
+ "./package.json": "./package.json"
51
+ },
52
+ "files": [
53
+ "lib",
54
+ "src",
55
+ "assets",
56
+ "cordis.patch.yml",
57
+ "README.md",
58
+ "README.zh.md",
59
+ "LICENSE"
60
+ ],
61
+ "dsh": {
62
+ "bundle": {
63
+ "patch": "./cordis.patch.yml"
64
+ },
65
+ "client": {
66
+ "inject": [
67
+ "@deepseek-ai/dsh-client-ui-renderer",
68
+ "@deepseek-ai/dsh-client-ui-settings",
69
+ "@deepseek-ai/dsh-client-ui-slots"
70
+ ],
71
+ "platform": "web"
72
+ }
73
+ },
74
+ "peerDependencies": {
75
+ "@deepseek-ai/cordis": "^4.0.4",
76
+ "@deepseek-ai/dsh-attachment": "^0.2.0-rc.2",
77
+ "@deepseek-ai/dsh-credentials": "^0.2.0-rc.2",
78
+ "@deepseek-ai/dsh-fs": "^0.2.0-rc.2",
79
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
80
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
81
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.2.0-rc.2",
82
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
83
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
84
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
85
+ "@deepseek-ai/schemastery": "^3.18.4",
86
+ "react": "^18.2.0",
87
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "@deepseek-ai/cordis": {
91
+ "optional": true
92
+ },
93
+ "@deepseek-ai/dsh-attachment": {
94
+ "optional": true
95
+ },
96
+ "@deepseek-ai/dsh-fs": {
97
+ "optional": true
98
+ },
99
+ "@deepseek-ai/dsh-llm": {
100
+ "optional": true
101
+ },
102
+ "@deepseek-ai/dsh-settings": {
103
+ "optional": true
104
+ },
105
+ "@deepseek-ai/dsh-system-prompt": {
106
+ "optional": true
107
+ },
108
+ "@deepseek-ai/dsh-tools": {
109
+ "optional": true
110
+ },
111
+ "react": {
112
+ "optional": true
113
+ },
114
+ "@deepseek-ai/dsh-credentials": {
115
+ "optional": true
116
+ },
117
+ "@deepseek-ai/dsh-host-webserver": {
118
+ "optional": true
119
+ },
120
+ "@deepseek-ai/dsh-llm-pi-ai": {
121
+ "optional": true
122
+ },
123
+ "@deepseek-ai/schemastery": {
124
+ "optional": true
125
+ },
126
+ "@deepseek-ai/cordis-plugin-loader": {
127
+ "optional": true
128
+ }
129
+ },
130
+ "devDependencies": {
131
+ "@deepseek-ai/cordis": "~4.0.4",
132
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
133
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
134
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.2",
135
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
136
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
137
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
138
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
139
+ "@deepseek-ai/dsh-client-ui-tool": "0.2.0-rc.2",
140
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
141
+ "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
142
+ "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
143
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
144
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.2",
145
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
146
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
147
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
148
+ "@deepseek-ai/schemastery": "~3.18.4",
149
+ "@types/node": "^22.20.0",
150
+ "@types/react": "~18.3.1",
151
+ "react": "^18.2.0",
152
+ "react-test-renderer": "18.3.1",
153
+ "tsdown": "^0.4.0",
154
+ "typescript": "^5.7.0",
155
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
156
+ "@deepseek-ai/dsh-config-editor": "0.2.0-rc.2",
157
+ "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
158
+ "@deepseek-ai/dsh-app-boot": "0.2.0-rc.2"
159
+ }
160
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Provider icons for the dsh-sub2api settings page.
3
+ *
4
+ * SVG source: lobehub/lobe-icons (`https://lobehub.com/icons`), the static SVG
5
+ * package. The three provider marks are embedded as React components so the
6
+ * settings page renders them without network requests.
7
+ *
8
+ * @module dsh-sub2api/client/icons
9
+ */
10
+
11
+ import type { CSSProperties } from 'react'
12
+
13
+ export type ProviderIconName = 'openai' | 'claude' | 'grok'
14
+
15
+ const paths: Record<ProviderIconName, string> = {
16
+ openai:
17
+ 'M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z',
18
+ claude:
19
+ 'M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z',
20
+ grok:
21
+ 'M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815',
22
+
23
+ }
24
+
25
+ export function ProviderIcon({ name, size = 18, style }: { name: ProviderIconName; size?: number; style?: CSSProperties }) {
26
+ return (
27
+ <svg
28
+ width={size}
29
+ height={size}
30
+ viewBox="0 0 24 24"
31
+ fill="currentColor"
32
+ style={{ flex: 'none', lineHeight: 1, ...style }}
33
+ aria-hidden="true"
34
+ >
35
+ <path d={paths[name]} fillRule="evenodd" />
36
+ </svg>
37
+ )
38
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Browser half of dsh-sub2api: registers the settings section.
3
+ *
4
+ * @module dsh-sub2api/client
5
+ */
6
+
7
+ import type { Context } from '@deepseek-ai/cordis'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
9
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
10
+ import { Sub2ApiSettings } from './settings.tsx'
11
+ import { GenerateImageToolview } from './toolview.tsx'
12
+
13
+ export const name = 'dsh-sub2api-client'
14
+ export const inject = ['slots']
15
+
16
+ export function apply(ctx: Context): void {
17
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
18
+ name: 'settings.section',
19
+ id: 'sub2api-models',
20
+ order: 12,
21
+ label: () => 'Sub2API 模型',
22
+ }, Sub2ApiSettings))
23
+
24
+ // Render generate_image results as an inline image inside the tool card:
25
+ // the tool result already carries an image content block (durable
26
+ // attachment), and this keyed toolview turns those bytes into an <img>.
27
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
28
+ name: 'tool.call.toolview',
29
+ key: 'generate_image',
30
+ }, GenerateImageToolview))
31
+ }