dsh-any-deepseek 0.0.8 → 0.0.9

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 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 attachmentId - durable id from the message block.
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?: (attachmentId: string, mediaType: string) => Promise<{
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/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,
@@ -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 = channel.reasoningEfforts === void 0 ? void 0 : Object.fromEntries(Object.entries(channel.reasoningEfforts).filter(([, value]) => value !== null));
689
- const hasThinking = thinkingLevelMap !== void 0 && Object.keys(thinkingLevelMap).length > 0;
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 id = candidate.attachment?.attachmentId;
736
- const mediaType = candidate.attachment?.mediaType;
737
- if (typeof id !== "string" || typeof mediaType !== "string") continue;
738
- if (images.has(id)) continue;
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 (item) => {
817
+ const resolved = await Promise.all(pending.map(async (ref) => {
746
818
  try {
747
- return [item.id, await readImage(item.id, item.mediaType)];
819
+ return [ref.attachmentId, await readImage(ref)];
748
820
  } catch {
749
- return [item.id, void 0];
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?.reasoningEfforts ?? {}).filter(([, wire]) => wire !== null).map(([id]) => id);
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,
@@ -1784,7 +1865,7 @@ function registerStatsRoutes(ctx, webServer, deps) {
1784
1865
  });
1785
1866
  return;
1786
1867
  }
1787
- const verdict = await probeChannel(channel, key);
1868
+ const verdict = await probeChannel(channel, key, `probe:${channel.id}`);
1788
1869
  json(res, verdict.ok ? 200 : 502, verdict);
1789
1870
  }
1790
1871
  }
@@ -1906,25 +1987,28 @@ function apply(ctx, config) {
1906
1987
  * 2048px keeps the harness's normal form, and the byte ceiling is the one the host's pi-ai
1907
1988
  * adapter uses for the same purpose.
1908
1989
  */
1909
- const readImage = async (attachmentId, mediaType) => {
1990
+ const readImage = async (ref) => {
1910
1991
  const attachments = service(ctx, "attachments");
1911
- if (attachments?.readImageRequest === void 0) return void 0;
1992
+ if (attachments?.readImageRequest === void 0) {
1993
+ console.warn("[any-deepseek] 图片无法读取:本组合没有装配 attachments 服务(readImageRequest 不可用)");
1994
+ return;
1995
+ }
1912
1996
  try {
1913
- const stored = await attachments.readImageRequest({
1914
- attachmentId,
1915
- mediaType
1916
- }, {
1997
+ const stored = await attachments.readImageRequest(ref, {
1917
1998
  width: 2048,
1918
1999
  height: 2048,
1919
2000
  maxBytes: 4194304
1920
2001
  });
1921
- if (stored?.data === void 0) return void 0;
2002
+ if (stored?.data === void 0) {
2003
+ console.warn(`[any-deepseek] 图片 ${ref.attachmentId} 读取结果为空`);
2004
+ return;
2005
+ }
1922
2006
  return {
1923
2007
  data: Buffer.from(stored.data).toString("base64"),
1924
- mediaType: stored.mediaType ?? mediaType
2008
+ mediaType: stored.mediaType ?? ref.mediaType
1925
2009
  };
1926
2010
  } catch (error) {
1927
- console.warn(`[any-deepseek] 读不到图片 ${attachmentId}: ${error instanceof Error ? error.message : String(error)}`);
2011
+ console.warn(`[any-deepseek] 读不到图片 ${ref.attachmentId}: ${error instanceof Error ? error.message : String(error)}`);
1928
2012
  return;
1929
2013
  }
1930
2014
  };
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.8",
3
+ "version": "0.0.9",
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": {