dsh-any-deepseek 0.0.8 → 0.0.10
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/lib/adapter.d.ts +66 -3
- package/lib/client.js +1 -1
- package/lib/http.d.ts +8 -1
- package/lib/index.js +139 -36
- package/lib/presets.d.ts +35 -0
- package/package.json +1 -1
package/lib/adapter.d.ts
CHANGED
|
@@ -27,12 +27,51 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
|
27
27
|
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, PreparedAdapterCall, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
28
28
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
29
29
|
import { type Api, type AssistantMessage, type AssistantMessageEvent, type Context as PiContext, type Model } from '@earendil-works/pi-ai';
|
|
30
|
+
import { type ChannelModality } from './presets.js';
|
|
30
31
|
import type { ResolvedChannel } from './channels.js';
|
|
31
32
|
import type { LedgerEntry } from './usage-ledger.js';
|
|
32
33
|
/** The single provider route this plugin owns. */
|
|
33
34
|
export declare const PROVIDER_ID = "any-deepseek";
|
|
34
35
|
/** Display name for the route in provider selectors. */
|
|
35
36
|
export declare const PROVIDER_NAME = "Any DeepSeek";
|
|
37
|
+
/**
|
|
38
|
+
* Input modalities this provider's one model accepts.
|
|
39
|
+
*
|
|
40
|
+
* 🔴 **Declared at registration, because the harness asks the model — not the channel.**
|
|
41
|
+
*
|
|
42
|
+
* `LlmModelInfo.inputModalities` is documented as "absent means unknown" (`dsh-llm/lib/types/
|
|
43
|
+
* types.d.ts:330-331`), and the host reads it at dispatch to decide whether an image survives
|
|
44
|
+
* projection at all: `if (modelInfo.inputModalities !== undefined &&
|
|
45
|
+
* !modelInfo.inputModalities.includes('image') && …) projectImagesForTextModel(…)`
|
|
46
|
+
* (`dsh-llm/lib/index.js:2337`). Omitting it therefore did not mean "we never send images"; it
|
|
47
|
+
* meant a user's screenshot was replaced by a placeholder *before* this plugin ever saw it, which
|
|
48
|
+
* is why the model answered as though the message had been text.
|
|
49
|
+
*
|
|
50
|
+
* The value is the MODEL's capability, so it does not depend on which channel is selected, on
|
|
51
|
+
* whether a key exists, or on whether `current` is set — the same rule as the catalog itself.
|
|
52
|
+
* A relay that genuinely cannot carry images declares `input: ['text']` on its own channel, and
|
|
53
|
+
* the transport (`pi-ai`) downgrades the image per channel; the harness-level answer stays the
|
|
54
|
+
* model's.
|
|
55
|
+
*/
|
|
56
|
+
export declare const MODEL_INPUT_MODALITIES: readonly ChannelModality[];
|
|
57
|
+
/**
|
|
58
|
+
* The reasoning levels in force for one channel.
|
|
59
|
+
*
|
|
60
|
+
* 🔴 **A channel that says nothing inherits the model's table, and that is the whole fix for the
|
|
61
|
+
* missing thinking control.**
|
|
62
|
+
*
|
|
63
|
+
* The old code read `channel.reasoningEfforts` alone. That value only existed once a channel
|
|
64
|
+
* declared it — so during first-run setup (no channel, or a channel written before the panel
|
|
65
|
+
* offered levels) the answer was "no reasoning at all", and the composer's strength selector was
|
|
66
|
+
* absent. But the model always had levels; the CHANNEL was merely silent about them.
|
|
67
|
+
*
|
|
68
|
+
* An empty map counts as silence, not as "explicitly unsupported": `z.dict()` produces `{}` when
|
|
69
|
+
* the panel writes the field, and treating that as a negative capability is how the picker broke
|
|
70
|
+
* with `INVALID_MODEL_REASONING` in the first place.
|
|
71
|
+
* @param channel - the selected channel, when there is one.
|
|
72
|
+
* @returns the levels to advertise and to send, keyed by level name.
|
|
73
|
+
*/
|
|
74
|
+
export declare function effectiveReasoningEfforts(channel: ResolvedChannel | undefined): Record<string, string | null>;
|
|
36
75
|
/** What the adapter needs from the plugin, so it can be tested without a harness context. */
|
|
37
76
|
export interface AdapterDeps {
|
|
38
77
|
/** Enabled, resolved channels in configuration order. */
|
|
@@ -56,11 +95,10 @@ export interface AdapterDeps {
|
|
|
56
95
|
* seam an image cannot be sent at all. When it is missing, image content is replaced by a text
|
|
57
96
|
* placeholder rather than silently dropped: the model should know an image existed even when it
|
|
58
97
|
* cannot see it.
|
|
59
|
-
* @param
|
|
60
|
-
* @param mediaType - the block's declared media type.
|
|
98
|
+
* @param ref - the block's attachment reference, passed through WHOLE.
|
|
61
99
|
* @returns base64 bytes and the media type to put on the wire.
|
|
62
100
|
*/
|
|
63
|
-
readImage?: (
|
|
101
|
+
readImage?: (ref: HarnessImageRef) => Promise<{
|
|
64
102
|
data: string;
|
|
65
103
|
mediaType: string;
|
|
66
104
|
} | undefined>;
|
|
@@ -69,6 +107,15 @@ export interface AdapterDeps {
|
|
|
69
107
|
/** Monotonic-ish millisecond clock for durations. */
|
|
70
108
|
elapsed: () => number;
|
|
71
109
|
}
|
|
110
|
+
/** A durable image reference as a harness message block carries it. */
|
|
111
|
+
export interface HarnessImageRef {
|
|
112
|
+
attachmentId: string;
|
|
113
|
+
mediaType: string;
|
|
114
|
+
bytes?: number;
|
|
115
|
+
width?: number;
|
|
116
|
+
height?: number;
|
|
117
|
+
name?: string;
|
|
118
|
+
}
|
|
72
119
|
/** One finished call, as the adapter observed it. */
|
|
73
120
|
interface CallOutcome {
|
|
74
121
|
ok: boolean;
|
|
@@ -94,6 +141,19 @@ interface CallOutcome {
|
|
|
94
141
|
* @returns a stable, non-empty session id.
|
|
95
142
|
*/
|
|
96
143
|
export declare function opencodeSessionId(sessionId: unknown): string;
|
|
144
|
+
/**
|
|
145
|
+
* Build the per-request headers one channel needs.
|
|
146
|
+
*
|
|
147
|
+
* 🔴 **Shared by BOTH paths that talk to a relay** — the runtime `stream()` and the panel's
|
|
148
|
+
* `/probe`. They used to differ: `stream()` injected the OpenCode session header and `/probe`
|
|
149
|
+
* did not, so "测试" reported 400 `MissingSessionID` for a channel that worked in real use. A
|
|
150
|
+
* probe that checks something other than what a real request does is not a probe; it is a second
|
|
151
|
+
* implementation of the truth, and the two will disagree.
|
|
152
|
+
* @param channel - the resolved channel.
|
|
153
|
+
* @param sessionId - the harness session id, when the caller has one.
|
|
154
|
+
* @returns headers to send, on top of whatever the channel already declares.
|
|
155
|
+
*/
|
|
156
|
+
export declare function requestHeadersFor(channel: ResolvedChannel, sessionId: unknown): Record<string, string>;
|
|
97
157
|
/**
|
|
98
158
|
* Build the pi-ai model entry for one channel.
|
|
99
159
|
*
|
|
@@ -178,6 +238,9 @@ export declare class AnyDeepSeekAdapter extends LlmAdapter {
|
|
|
178
238
|
* ⇒ The catalog answers "what does this provider serve", which is a property of the plugin.
|
|
179
239
|
* Key presence is answered per request (`MISSING_CREDENTIAL`) and per row in the panel
|
|
180
240
|
* (`keyConfigured`), where the user can actually act on it.
|
|
241
|
+
*
|
|
242
|
+
* The same reasoning extends to `inputModalities`: it describes the model this route serves,
|
|
243
|
+
* so it is reported here unconditionally rather than derived from a channel.
|
|
181
244
|
*/
|
|
182
245
|
listModels(_provider: string): Promise<LlmModelInfo[]>;
|
|
183
246
|
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
package/lib/client.js
CHANGED
|
@@ -769,7 +769,7 @@ window.__ModuleLoader__.load({
|
|
|
769
769
|
children: "合计"
|
|
770
770
|
})
|
|
771
771
|
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [stats.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
772
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.key }),
|
|
772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.label ?? row.key }),
|
|
773
773
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
774
774
|
className: "ad-num",
|
|
775
775
|
children: row.aggregate.requests
|
package/lib/http.d.ts
CHANGED
|
@@ -64,12 +64,19 @@ export interface WebServerLike {
|
|
|
64
64
|
*
|
|
65
65
|
* This spends tokens, which is why it is an explicit route rather than something the panel does
|
|
66
66
|
* on render. The request asks for one output token, so the cost is a prompt plus a token.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 **The headers come from `requestHeadersFor`, the same function the runtime uses.** They used
|
|
69
|
+
* to be built here separately, which meant this probe sent no OpenCode session header while real
|
|
70
|
+
* requests did — so "测试" reported 400 `MissingSessionID` on a channel that worked in use. A
|
|
71
|
+
* probe that does not send what a real request sends is not testing the channel.
|
|
67
72
|
* @param channel - the channel to probe.
|
|
68
73
|
* @param key - its resolved credential.
|
|
74
|
+
* @param sessionId - session identity for per-request headers (the probe has no conversation of
|
|
75
|
+
* its own, so callers pass the selected channel's own stable value).
|
|
69
76
|
* @param signal - cancellation.
|
|
70
77
|
* @returns a verdict and, on success, what the relay reported.
|
|
71
78
|
*/
|
|
72
|
-
export declare function probeChannel(channel: ResolvedChannel, key: string, signal?: AbortSignal): Promise<{
|
|
79
|
+
export declare function probeChannel(channel: ResolvedChannel, key: string, sessionId?: string, signal?: AbortSignal): Promise<{
|
|
73
80
|
ok: boolean;
|
|
74
81
|
ms: number;
|
|
75
82
|
code?: string;
|
package/lib/index.js
CHANGED
|
@@ -170,6 +170,13 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
170
170
|
* images" — it meant the user's image never reached the model at all, silently. The relay side is
|
|
171
171
|
* now real: `adapter.ts` collects image bytes through the durable attachment store and maps them
|
|
172
172
|
* to pi-ai `image` content, and falls back to a visible text placeholder when it cannot.
|
|
173
|
+
*
|
|
174
|
+
* ## Why this is exported
|
|
175
|
+
*
|
|
176
|
+
* The adapter must report the SAME capabilities through `listModels()` / `resolveModel()`, which
|
|
177
|
+
* describe the advertised model rather than any one channel. Reaching for this constant (instead
|
|
178
|
+
* of re-listing the numbers there) keeps the catalog and the exact-model answer from drifting —
|
|
179
|
+
* and they are read by different host paths, so a drift is silent.
|
|
173
180
|
*/
|
|
174
181
|
const V41_FLASH_CAPS = {
|
|
175
182
|
contextWindow: 1e6,
|
|
@@ -281,7 +288,7 @@ const CHANNEL_PRESETS = {
|
|
|
281
288
|
displayName: "DeepSeek Official",
|
|
282
289
|
api: "openai-completions",
|
|
283
290
|
baseURL: "https://api.deepseek.com",
|
|
284
|
-
model: "deepseek-
|
|
291
|
+
model: "deepseek-flash",
|
|
285
292
|
...V41_FLASH_CAPS
|
|
286
293
|
}
|
|
287
294
|
};
|
|
@@ -654,6 +661,48 @@ const PROVIDER_ID = "any-deepseek";
|
|
|
654
661
|
/** Display name for the route in provider selectors. */
|
|
655
662
|
const PROVIDER_NAME = "Any DeepSeek";
|
|
656
663
|
/**
|
|
664
|
+
* Input modalities this provider's one model accepts.
|
|
665
|
+
*
|
|
666
|
+
* 🔴 **Declared at registration, because the harness asks the model — not the channel.**
|
|
667
|
+
*
|
|
668
|
+
* `LlmModelInfo.inputModalities` is documented as "absent means unknown" (`dsh-llm/lib/types/
|
|
669
|
+
* types.d.ts:330-331`), and the host reads it at dispatch to decide whether an image survives
|
|
670
|
+
* projection at all: `if (modelInfo.inputModalities !== undefined &&
|
|
671
|
+
* !modelInfo.inputModalities.includes('image') && …) projectImagesForTextModel(…)`
|
|
672
|
+
* (`dsh-llm/lib/index.js:2337`). Omitting it therefore did not mean "we never send images"; it
|
|
673
|
+
* meant a user's screenshot was replaced by a placeholder *before* this plugin ever saw it, which
|
|
674
|
+
* is why the model answered as though the message had been text.
|
|
675
|
+
*
|
|
676
|
+
* The value is the MODEL's capability, so it does not depend on which channel is selected, on
|
|
677
|
+
* whether a key exists, or on whether `current` is set — the same rule as the catalog itself.
|
|
678
|
+
* A relay that genuinely cannot carry images declares `input: ['text']` on its own channel, and
|
|
679
|
+
* the transport (`pi-ai`) downgrades the image per channel; the harness-level answer stays the
|
|
680
|
+
* model's.
|
|
681
|
+
*/
|
|
682
|
+
const MODEL_INPUT_MODALITIES = V41_FLASH_CAPS.input;
|
|
683
|
+
/**
|
|
684
|
+
* The reasoning levels in force for one channel.
|
|
685
|
+
*
|
|
686
|
+
* 🔴 **A channel that says nothing inherits the model's table, and that is the whole fix for the
|
|
687
|
+
* missing thinking control.**
|
|
688
|
+
*
|
|
689
|
+
* The old code read `channel.reasoningEfforts` alone. That value only existed once a channel
|
|
690
|
+
* declared it — so during first-run setup (no channel, or a channel written before the panel
|
|
691
|
+
* offered levels) the answer was "no reasoning at all", and the composer's strength selector was
|
|
692
|
+
* absent. But the model always had levels; the CHANNEL was merely silent about them.
|
|
693
|
+
*
|
|
694
|
+
* An empty map counts as silence, not as "explicitly unsupported": `z.dict()` produces `{}` when
|
|
695
|
+
* the panel writes the field, and treating that as a negative capability is how the picker broke
|
|
696
|
+
* with `INVALID_MODEL_REASONING` in the first place.
|
|
697
|
+
* @param channel - the selected channel, when there is one.
|
|
698
|
+
* @returns the levels to advertise and to send, keyed by level name.
|
|
699
|
+
*/
|
|
700
|
+
function effectiveReasoningEfforts(channel) {
|
|
701
|
+
const declared = channel?.reasoningEfforts;
|
|
702
|
+
if (declared !== void 0 && Object.keys(declared).length > 0) return declared;
|
|
703
|
+
return V41_FLASH_CAPS.reasoningEfforts;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
657
706
|
* Derive the value for the OpenCode session header.
|
|
658
707
|
*
|
|
659
708
|
* 🔴 This is THIS PLUGIN's job, not the harness's. `/zen/go` is one relay's private requirement;
|
|
@@ -677,6 +726,28 @@ function opencodeSessionId(sessionId) {
|
|
|
677
726
|
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
678
727
|
}
|
|
679
728
|
/**
|
|
729
|
+
* Build the per-request headers one channel needs.
|
|
730
|
+
*
|
|
731
|
+
* 🔴 **Shared by BOTH paths that talk to a relay** — the runtime `stream()` and the panel's
|
|
732
|
+
* `/probe`. They used to differ: `stream()` injected the OpenCode session header and `/probe`
|
|
733
|
+
* did not, so "测试" reported 400 `MissingSessionID` for a channel that worked in real use. A
|
|
734
|
+
* probe that checks something other than what a real request does is not a probe; it is a second
|
|
735
|
+
* implementation of the truth, and the two will disagree.
|
|
736
|
+
* @param channel - the resolved channel.
|
|
737
|
+
* @param sessionId - the harness session id, when the caller has one.
|
|
738
|
+
* @returns headers to send, on top of whatever the channel already declares.
|
|
739
|
+
*/
|
|
740
|
+
function requestHeadersFor(channel, sessionId) {
|
|
741
|
+
const headers = {};
|
|
742
|
+
if (channel.headers !== void 0 && Object.hasOwn(channel.headers, "x-opencode-client")) {
|
|
743
|
+
const session = opencodeSessionId(sessionId);
|
|
744
|
+
headers[OPENCODE_SESSION_HEADER] = session;
|
|
745
|
+
headers["X-Session-ID"] = session;
|
|
746
|
+
headers["User-Agent"] = CLIENT_USER_AGENT;
|
|
747
|
+
}
|
|
748
|
+
return headers;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
680
751
|
* Build the pi-ai model entry for one channel.
|
|
681
752
|
*
|
|
682
753
|
* The advertised id (`deepseek-v4.1-flash`) never reaches the wire here: each channel's own
|
|
@@ -685,8 +756,12 @@ function opencodeSessionId(sessionId) {
|
|
|
685
756
|
* @returns a pi-ai chat model describing that relay's view of the model.
|
|
686
757
|
*/
|
|
687
758
|
function buildPiModel(channel) {
|
|
688
|
-
const thinkingLevelMap =
|
|
689
|
-
|
|
759
|
+
const thinkingLevelMap = (() => {
|
|
760
|
+
const declared = {};
|
|
761
|
+
for (const [level, wire] of Object.entries(effectiveReasoningEfforts(channel))) if (typeof wire === "string") declared[level] = wire;
|
|
762
|
+
return Object.keys(declared).length === 0 ? void 0 : declared;
|
|
763
|
+
})();
|
|
764
|
+
const hasThinking = thinkingLevelMap !== void 0;
|
|
690
765
|
return {
|
|
691
766
|
id: channel.model,
|
|
692
767
|
name: channel.label,
|
|
@@ -732,21 +807,18 @@ async function collectImages(options, readImage) {
|
|
|
732
807
|
for (const block of message.content) {
|
|
733
808
|
const candidate = block;
|
|
734
809
|
if (candidate.type !== "image") continue;
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
if (
|
|
738
|
-
if (
|
|
739
|
-
pending.push(
|
|
740
|
-
id,
|
|
741
|
-
mediaType
|
|
742
|
-
});
|
|
810
|
+
const ref = candidate.attachment;
|
|
811
|
+
if (ref === void 0 || typeof ref.attachmentId !== "string") continue;
|
|
812
|
+
if (images.has(ref.attachmentId)) continue;
|
|
813
|
+
if (pending.some((item) => item.attachmentId === ref.attachmentId)) continue;
|
|
814
|
+
pending.push(ref);
|
|
743
815
|
}
|
|
744
816
|
}
|
|
745
|
-
const resolved = await Promise.all(pending.map(async (
|
|
817
|
+
const resolved = await Promise.all(pending.map(async (ref) => {
|
|
746
818
|
try {
|
|
747
|
-
return [
|
|
819
|
+
return [ref.attachmentId, await readImage(ref)];
|
|
748
820
|
} catch {
|
|
749
|
-
return [
|
|
821
|
+
return [ref.attachmentId, void 0];
|
|
750
822
|
}
|
|
751
823
|
}));
|
|
752
824
|
for (const [id, value] of resolved) if (value !== void 0) images.set(id, value);
|
|
@@ -1145,6 +1217,9 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1145
1217
|
* ⇒ The catalog answers "what does this provider serve", which is a property of the plugin.
|
|
1146
1218
|
* Key presence is answered per request (`MISSING_CREDENTIAL`) and per row in the panel
|
|
1147
1219
|
* (`keyConfigured`), where the user can actually act on it.
|
|
1220
|
+
*
|
|
1221
|
+
* The same reasoning extends to `inputModalities`: it describes the model this route serves,
|
|
1222
|
+
* so it is reported here unconditionally rather than derived from a channel.
|
|
1148
1223
|
*/
|
|
1149
1224
|
async listModels(_provider) {
|
|
1150
1225
|
const selected = this.selected();
|
|
@@ -1154,12 +1229,13 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1154
1229
|
provider: PROVIDER_ID,
|
|
1155
1230
|
id: ADVERTISED_MODEL_ID,
|
|
1156
1231
|
name: ADVERTISED_MODEL_NAME,
|
|
1157
|
-
description: label === void 0 ? "no channel configured yet" : `via ${label}
|
|
1232
|
+
description: label === void 0 ? "no channel configured yet" : `via ${label}`,
|
|
1233
|
+
inputModalities: [...MODEL_INPUT_MODALITIES]
|
|
1158
1234
|
}];
|
|
1159
1235
|
}
|
|
1160
1236
|
async resolveModel(provider, model, _signal) {
|
|
1161
1237
|
const channel = this.selected();
|
|
1162
|
-
const efforts = Object.entries(channel
|
|
1238
|
+
const efforts = Object.entries(effectiveReasoningEfforts(channel)).filter(([, wire]) => wire !== null).map(([id]) => id);
|
|
1163
1239
|
const reasoning = efforts.length === 0 ? {} : { reasoning: { efforts: efforts.map((id) => ({
|
|
1164
1240
|
id: ReasoningEffortId(id),
|
|
1165
1241
|
name: id
|
|
@@ -1171,6 +1247,7 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1171
1247
|
description: channel === void 0 ? void 0 : `via ${channel.label}`,
|
|
1172
1248
|
context: { contextWindow: channel?.contextWindow ?? 1e6 },
|
|
1173
1249
|
defaultMaxTokens: channel?.maxTokens,
|
|
1250
|
+
inputModalities: [...MODEL_INPUT_MODALITIES],
|
|
1174
1251
|
...reasoning
|
|
1175
1252
|
};
|
|
1176
1253
|
}
|
|
@@ -1269,14 +1346,10 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1269
1346
|
* for the same conversation, which is all the endpoint's affinity key needs.
|
|
1270
1347
|
* · `User-Agent` must identify THIS client rather than the generic SDK. pi-ai sets its own,
|
|
1271
1348
|
* so the preset's static headers alone can never satisfy that rule.
|
|
1349
|
+
*
|
|
1350
|
+
* The construction lives in `requestHeadersFor` so the panel's probe sends the same thing.
|
|
1272
1351
|
*/
|
|
1273
|
-
const requestHeaders =
|
|
1274
|
-
if (channel.headers !== void 0 && Object.hasOwn(channel.headers, "x-opencode-client")) {
|
|
1275
|
-
const session = opencodeSessionId(options.sessionId);
|
|
1276
|
-
requestHeaders[OPENCODE_SESSION_HEADER] = session;
|
|
1277
|
-
requestHeaders["X-Session-ID"] = session;
|
|
1278
|
-
requestHeaders["User-Agent"] = CLIENT_USER_AGENT;
|
|
1279
|
-
}
|
|
1352
|
+
const requestHeaders = requestHeadersFor(channel, options.sessionId);
|
|
1280
1353
|
const streamOptions = {
|
|
1281
1354
|
apiKey: key,
|
|
1282
1355
|
signal: options.signal,
|
|
@@ -1609,12 +1682,19 @@ function queryDate(value) {
|
|
|
1609
1682
|
*
|
|
1610
1683
|
* This spends tokens, which is why it is an explicit route rather than something the panel does
|
|
1611
1684
|
* on render. The request asks for one output token, so the cost is a prompt plus a token.
|
|
1685
|
+
*
|
|
1686
|
+
* 🔴 **The headers come from `requestHeadersFor`, the same function the runtime uses.** They used
|
|
1687
|
+
* to be built here separately, which meant this probe sent no OpenCode session header while real
|
|
1688
|
+
* requests did — so "测试" reported 400 `MissingSessionID` on a channel that worked in use. A
|
|
1689
|
+
* probe that does not send what a real request sends is not testing the channel.
|
|
1612
1690
|
* @param channel - the channel to probe.
|
|
1613
1691
|
* @param key - its resolved credential.
|
|
1692
|
+
* @param sessionId - session identity for per-request headers (the probe has no conversation of
|
|
1693
|
+
* its own, so callers pass the selected channel's own stable value).
|
|
1614
1694
|
* @param signal - cancellation.
|
|
1615
1695
|
* @returns a verdict and, on success, what the relay reported.
|
|
1616
1696
|
*/
|
|
1617
|
-
async function probeChannel(channel, key, signal) {
|
|
1697
|
+
async function probeChannel(channel, key, sessionId, signal) {
|
|
1618
1698
|
const started = Date.now();
|
|
1619
1699
|
const url = `${channel.baseURL.replace(/\/+$/, "")}/chat/completions`;
|
|
1620
1700
|
try {
|
|
@@ -1623,7 +1703,8 @@ async function probeChannel(channel, key, signal) {
|
|
|
1623
1703
|
headers: {
|
|
1624
1704
|
"content-type": "application/json",
|
|
1625
1705
|
authorization: `Bearer ${key}`,
|
|
1626
|
-
...channel.headers
|
|
1706
|
+
...channel.headers,
|
|
1707
|
+
...requestHeadersFor(channel, sessionId)
|
|
1627
1708
|
},
|
|
1628
1709
|
body: JSON.stringify({
|
|
1629
1710
|
model: channel.model,
|
|
@@ -1739,13 +1820,32 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
1739
1820
|
const group = url.searchParams.get("group") ?? "channel";
|
|
1740
1821
|
const entries = await readLedgerRange(deps.ledgerDir(), from, to);
|
|
1741
1822
|
const rows = group === "day" ? summarize(entries, (entry) => dayKey(new Date(entry.at))) : summarize(entries, (entry) => entry.channel);
|
|
1823
|
+
/**
|
|
1824
|
+
* 🔴 Label each row with the channel's DISPLAY NAME, not its id.
|
|
1825
|
+
*
|
|
1826
|
+
* Owner report (2026-10-22): 「统计要按显示名称来」. The ledger keys rows by the channel
|
|
1827
|
+
* id because that is the channel's identity — it must not change when a user renames a
|
|
1828
|
+
* channel (decision 37: renaming touches `label`, never `id`). But the id is an
|
|
1829
|
+
* implementation handle, and showing it makes the table disagree with the list above it,
|
|
1830
|
+
* where the same channel appears under its display name.
|
|
1831
|
+
*
|
|
1832
|
+
* Resolution is done here, at read time, from the LIVE channel list — so a rename is
|
|
1833
|
+
* reflected immediately in historical rows too, with no ledger rewrite. A row whose
|
|
1834
|
+
* channel no longer exists keeps its raw id: that usage is real and must stay visible,
|
|
1835
|
+
* and inventing a name for a channel that is gone would be worse than the id.
|
|
1836
|
+
*/
|
|
1837
|
+
const labels = new Map(deps.channels().map((channel) => [channel.id, channel.label]));
|
|
1838
|
+
const named = rows.map((row) => ({
|
|
1839
|
+
...row,
|
|
1840
|
+
label: group === "day" ? row.key : labels.get(row.key) ?? row.key
|
|
1841
|
+
}));
|
|
1742
1842
|
json(res, 200, {
|
|
1743
1843
|
range: {
|
|
1744
1844
|
from: from?.toISOString() ?? null,
|
|
1745
1845
|
to: to?.toISOString() ?? null,
|
|
1746
1846
|
group
|
|
1747
1847
|
},
|
|
1748
|
-
rows,
|
|
1848
|
+
rows: named,
|
|
1749
1849
|
total: totalOf(rows),
|
|
1750
1850
|
note: "input is uncached input only; billable input = input + cacheRead + cacheWrite"
|
|
1751
1851
|
});
|
|
@@ -1784,7 +1884,7 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
1784
1884
|
});
|
|
1785
1885
|
return;
|
|
1786
1886
|
}
|
|
1787
|
-
const verdict = await probeChannel(channel, key);
|
|
1887
|
+
const verdict = await probeChannel(channel, key, `probe:${channel.id}`);
|
|
1788
1888
|
json(res, verdict.ok ? 200 : 502, verdict);
|
|
1789
1889
|
}
|
|
1790
1890
|
}
|
|
@@ -1906,25 +2006,28 @@ function apply(ctx, config) {
|
|
|
1906
2006
|
* 2048px keeps the harness's normal form, and the byte ceiling is the one the host's pi-ai
|
|
1907
2007
|
* adapter uses for the same purpose.
|
|
1908
2008
|
*/
|
|
1909
|
-
const readImage = async (
|
|
2009
|
+
const readImage = async (ref) => {
|
|
1910
2010
|
const attachments = service(ctx, "attachments");
|
|
1911
|
-
if (attachments?.readImageRequest === void 0)
|
|
2011
|
+
if (attachments?.readImageRequest === void 0) {
|
|
2012
|
+
console.warn("[any-deepseek] 图片无法读取:本组合没有装配 attachments 服务(readImageRequest 不可用)");
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
1912
2015
|
try {
|
|
1913
|
-
const stored = await attachments.readImageRequest({
|
|
1914
|
-
attachmentId,
|
|
1915
|
-
mediaType
|
|
1916
|
-
}, {
|
|
2016
|
+
const stored = await attachments.readImageRequest(ref, {
|
|
1917
2017
|
width: 2048,
|
|
1918
2018
|
height: 2048,
|
|
1919
2019
|
maxBytes: 4194304
|
|
1920
2020
|
});
|
|
1921
|
-
if (stored?.data === void 0)
|
|
2021
|
+
if (stored?.data === void 0) {
|
|
2022
|
+
console.warn(`[any-deepseek] 图片 ${ref.attachmentId} 读取结果为空`);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1922
2025
|
return {
|
|
1923
2026
|
data: Buffer.from(stored.data).toString("base64"),
|
|
1924
|
-
mediaType: stored.mediaType ?? mediaType
|
|
2027
|
+
mediaType: stored.mediaType ?? ref.mediaType
|
|
1925
2028
|
};
|
|
1926
2029
|
} catch (error) {
|
|
1927
|
-
console.warn(`[any-deepseek] 读不到图片 ${attachmentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2030
|
+
console.warn(`[any-deepseek] 读不到图片 ${ref.attachmentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1928
2031
|
return;
|
|
1929
2032
|
}
|
|
1930
2033
|
};
|
package/lib/presets.d.ts
CHANGED
|
@@ -81,6 +81,41 @@ export declare const ADVERTISED_MODEL_NAME = "DeepSeek V4.1 Flash";
|
|
|
81
81
|
*/
|
|
82
82
|
export declare const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
83
83
|
export declare const DEFAULT_MAX_TOKENS = 32768;
|
|
84
|
+
/**
|
|
85
|
+
* Capability numbers shared by every DeepSeek V4.1 Flash deployment we know of.
|
|
86
|
+
*
|
|
87
|
+
* Kept as one constant because these are properties of the *model*, not of a relay; a relay
|
|
88
|
+
* that disagrees corrects them in its own preset.
|
|
89
|
+
*
|
|
90
|
+
* ## Why `input` lists image
|
|
91
|
+
*
|
|
92
|
+
* The model is multimodal, and the harness only projects an image into a request when the model
|
|
93
|
+
* entry declares the capability. Declaring text-only therefore did not mean "we never send
|
|
94
|
+
* images" — it meant the user's image never reached the model at all, silently. The relay side is
|
|
95
|
+
* now real: `adapter.ts` collects image bytes through the durable attachment store and maps them
|
|
96
|
+
* to pi-ai `image` content, and falls back to a visible text placeholder when it cannot.
|
|
97
|
+
*
|
|
98
|
+
* ## Why this is exported
|
|
99
|
+
*
|
|
100
|
+
* The adapter must report the SAME capabilities through `listModels()` / `resolveModel()`, which
|
|
101
|
+
* describe the advertised model rather than any one channel. Reaching for this constant (instead
|
|
102
|
+
* of re-listing the numbers there) keeps the catalog and the exact-model answer from drifting —
|
|
103
|
+
* and they are read by different host paths, so a drift is silent.
|
|
104
|
+
*/
|
|
105
|
+
export declare const V41_FLASH_CAPS: {
|
|
106
|
+
contextWindow: number;
|
|
107
|
+
maxTokens: number;
|
|
108
|
+
input: ChannelModality[];
|
|
109
|
+
/**
|
|
110
|
+
* Selectable thinking levels, in pi-ai's vocabulary.
|
|
111
|
+
*
|
|
112
|
+
* The VALUES are the wire spellings this model family accepts (`reasoning_effort`); `null`
|
|
113
|
+
* would mark a level as unsupported. pi-ai consumes this map directly as `thinkingLevelMap`,
|
|
114
|
+
* which is why this plugin does not implement a translation layer of its own — a second
|
|
115
|
+
* mapping would inevitably drift from the one the transport actually applies.
|
|
116
|
+
*/
|
|
117
|
+
reasoningEfforts: Record<string, string | null>;
|
|
118
|
+
};
|
|
84
119
|
/**
|
|
85
120
|
* The user agent a request to OpenCode Go identifies itself with.
|
|
86
121
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-deepseek",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|