dsh-any-deepseek 0.0.7 β 0.0.8
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 +58 -3
- package/lib/index.js +213 -41
- package/lib/presets.d.ts +17 -1
- package/package.json +1 -1
package/lib/adapter.d.ts
CHANGED
|
@@ -48,6 +48,22 @@ export interface AdapterDeps {
|
|
|
48
48
|
resolveKey: (ref: string) => Promise<string | undefined>;
|
|
49
49
|
/** Record one finished request. Must never throw. */
|
|
50
50
|
record: (entry: LedgerEntry) => Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Read one image's request bytes from the durable attachment store.
|
|
53
|
+
*
|
|
54
|
+
* π΄ Optional, and its absence is a real state (a composition without the attachment service).
|
|
55
|
+
* An `ImageBlock` in the harness carries only an `attachmentId` β never bytes β so without this
|
|
56
|
+
* seam an image cannot be sent at all. When it is missing, image content is replaced by a text
|
|
57
|
+
* placeholder rather than silently dropped: the model should know an image existed even when it
|
|
58
|
+
* cannot see it.
|
|
59
|
+
* @param attachmentId - durable id from the message block.
|
|
60
|
+
* @param mediaType - the block's declared media type.
|
|
61
|
+
* @returns base64 bytes and the media type to put on the wire.
|
|
62
|
+
*/
|
|
63
|
+
readImage?: (attachmentId: string, mediaType: string) => Promise<{
|
|
64
|
+
data: string;
|
|
65
|
+
mediaType: string;
|
|
66
|
+
} | undefined>;
|
|
51
67
|
/** Clock, injectable so tests are deterministic. */
|
|
52
68
|
now: () => Date;
|
|
53
69
|
/** Monotonic-ish millisecond clock for durations. */
|
|
@@ -59,6 +75,25 @@ interface CallOutcome {
|
|
|
59
75
|
code?: string;
|
|
60
76
|
usage: TokenUsage | null;
|
|
61
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Derive the value for the OpenCode session header.
|
|
80
|
+
*
|
|
81
|
+
* π΄ This is THIS PLUGIN's job, not the harness's. `/zen/go` is one relay's private requirement;
|
|
82
|
+
* the host cannot know about it and must not be expected to stamp anything for it. Whatever the
|
|
83
|
+
* harness happens to put on a request is only an input to a decision made here.
|
|
84
|
+
*
|
|
85
|
+
* The id is a stable hash of the harness session id, so:
|
|
86
|
+
* Β· the same conversation keeps one affinity key (what the endpoint wants), and
|
|
87
|
+
* Β· the value is opaque rather than echoing an internal id to a third party.
|
|
88
|
+
*
|
|
89
|
+
* Two hard requirements, both learned from a 400:
|
|
90
|
+
* Β· **never absent** β the header is always sent, so a request can never go out without it;
|
|
91
|
+
* Β· **stable when there is no session** β falling back to a random value would give every
|
|
92
|
+
* request a different affinity key, which is worse than a constant.
|
|
93
|
+
* @param sessionId - the harness session id, when it supplied one.
|
|
94
|
+
* @returns a stable, non-empty session id.
|
|
95
|
+
*/
|
|
96
|
+
export declare function opencodeSessionId(sessionId: unknown): string;
|
|
62
97
|
/**
|
|
63
98
|
* Build the pi-ai model entry for one channel.
|
|
64
99
|
*
|
|
@@ -68,15 +103,35 @@ interface CallOutcome {
|
|
|
68
103
|
* @returns a pi-ai chat model describing that relay's view of the model.
|
|
69
104
|
*/
|
|
70
105
|
export declare function buildPiModel(channel: ResolvedChannel): Model<Api>;
|
|
106
|
+
/** One resolved image, keyed by attachment id. */
|
|
107
|
+
export type ImageBytes = Map<string, {
|
|
108
|
+
data: string;
|
|
109
|
+
mediaType: string;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Collect the bytes for every image in a request.
|
|
113
|
+
*
|
|
114
|
+
* π΄ A separate pass, because the conversion itself is synchronous and the store is not. This is
|
|
115
|
+
* also how the host's own pi-ai adapter does it, for the same reason.
|
|
116
|
+
*
|
|
117
|
+
* A block that cannot be read is left OUT of the map, and the converter then substitutes a text
|
|
118
|
+
* placeholder. That is deliberate: dropping the block would tell the model nothing, while a
|
|
119
|
+
* placeholder at least says an image was attached and names why it could not be shown. Skipping
|
|
120
|
+
* it silently is the failure mode this whole change exists to remove β the user sent an image and
|
|
121
|
+
* the model answered as though the message had been text only.
|
|
122
|
+
* @param options - the harness request.
|
|
123
|
+
* @param readImage - the attachment seam, when the composition has one.
|
|
124
|
+
* @returns resolved images keyed by attachment id.
|
|
125
|
+
*/
|
|
126
|
+
export declare function collectImages(options: GenerateOptions, readImage: AdapterDeps['readImage']): Promise<ImageBytes>;
|
|
71
127
|
/**
|
|
72
128
|
* Translate one harness request into a pi-ai context.
|
|
73
129
|
*
|
|
74
|
-
* The harness already projects images and files before an adapter sees them, so this is a
|
|
75
|
-
* straightforward content mapping; anything the harness did not project is passed through.
|
|
76
130
|
* @param options - the harness request.
|
|
131
|
+
* @param images - image bytes collected by {@link collectImages}; omitted for a text-only request.
|
|
77
132
|
* @returns a pi-ai context plus the one-shot system prompt, when the caller set one.
|
|
78
133
|
*/
|
|
79
|
-
export declare function toPiContext(options: GenerateOptions): PiContext;
|
|
134
|
+
export declare function toPiContext(options: GenerateOptions, images?: ImageBytes): PiContext;
|
|
80
135
|
/** Map pi-ai usage onto the harness's disjoint counters. */
|
|
81
136
|
export declare function toTokenUsage(usage: AssistantMessage['usage']): TokenUsage;
|
|
82
137
|
/**
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { LlmAdapter, ReasoningEffortId, ToolCallId } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import "@deepseek-ai/dsh-credentials";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { createModels, createProvider } from "@earendil-works/pi-ai";
|
|
6
7
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
7
8
|
import { appendFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
@@ -162,17 +163,18 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
162
163
|
* Kept as one constant because these are properties of the *model*, not of a relay; a relay
|
|
163
164
|
* that disagrees corrects them in its own preset.
|
|
164
165
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
166
|
+
* ## Why `input` lists image
|
|
167
|
+
*
|
|
168
|
+
* The model is multimodal, and the harness only projects an image into a request when the model
|
|
169
|
+
* entry declares the capability. Declaring text-only therefore did not mean "we never send
|
|
170
|
+
* images" β it meant the user's image never reached the model at all, silently. The relay side is
|
|
171
|
+
* now real: `adapter.ts` collects image bytes through the durable attachment store and maps them
|
|
172
|
+
* to pi-ai `image` content, and falls back to a visible text placeholder when it cannot.
|
|
171
173
|
*/
|
|
172
174
|
const V41_FLASH_CAPS = {
|
|
173
175
|
contextWindow: 1e6,
|
|
174
176
|
maxTokens: 384e3,
|
|
175
|
-
input: ["text"],
|
|
177
|
+
input: ["text", "image"],
|
|
176
178
|
/**
|
|
177
179
|
* Selectable thinking levels, in pi-ai's vocabulary.
|
|
178
180
|
*
|
|
@@ -201,21 +203,12 @@ const DEEPSEEK_THINKING_COMPAT = {
|
|
|
201
203
|
thinkingFormat: "deepseek"
|
|
202
204
|
};
|
|
203
205
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* `/zen/go` answers 400 `Request is missing x-opencode-session` without it, and the official
|
|
207
|
-
* documentation adds two requirements this preset has to respect:
|
|
208
|
-
*
|
|
209
|
-
* 1. the session id must be **stable per conversation** β a constant shared by every
|
|
210
|
-
* conversation violates the thing the header exists for (routing and cache affinity);
|
|
211
|
-
* 2. the client must **identify itself with its own user agent** rather than a generic SDK name.
|
|
212
|
-
*
|
|
213
|
-
* So the session id is deliberately NOT here: `adapter.ts` injects it per request from the
|
|
214
|
-
* harness's own session identity. What stays here is the static identity of this client.
|
|
206
|
+
* Static headers the OpenCode Go endpoint wants on every request.
|
|
215
207
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
208
|
+
* The session id is NOT here on purpose β it is injected per request, because the endpoint wants
|
|
209
|
+
* it stable *per conversation* rather than per install. What lives here is the client identity,
|
|
210
|
+
* which does not vary. See {@link OPENCODE_SESSION_FALLBACK} for what happens when the harness
|
|
211
|
+
* gives us no session at all.
|
|
219
212
|
*/
|
|
220
213
|
const OPENCODE_HEADERS = {
|
|
221
214
|
"x-opencode-client": "dsh",
|
|
@@ -224,12 +217,28 @@ const OPENCODE_HEADERS = {
|
|
|
224
217
|
/**
|
|
225
218
|
* The user agent a request to OpenCode Go identifies itself with.
|
|
226
219
|
*
|
|
227
|
-
*
|
|
220
|
+
* The upstream asks clients to identify themselves rather than send a generic SDK name.
|
|
228
221
|
*/
|
|
229
222
|
const CLIENT_USER_AGENT = "dsh-any-deepseek/1.0";
|
|
230
223
|
/** Header carrying the per-conversation routing id on OpenCode Go. */
|
|
231
224
|
const OPENCODE_SESSION_HEADER = "x-opencode-session";
|
|
232
225
|
/**
|
|
226
|
+
* Session id used when the harness gives us none.
|
|
227
|
+
*
|
|
228
|
+
* π΄ **This fallback is load-bearing, not decoration**, and dropping it is a measured failure.
|
|
229
|
+
*
|
|
230
|
+
* The endpoint wants a per-conversation id, so the honest implementation is "use the harness's
|
|
231
|
+
* `sessionId`". The problem is that the harness does not stamp one on every request β and when it
|
|
232
|
+
* does not, an implementation that ONLY reads `options.sessionId` sends **no session header at
|
|
233
|
+
* all**, which the endpoint rejects with 400 `MissingSessionID`: precisely the failure the header
|
|
234
|
+
* exists to prevent. So the value is "harness session when present, this constant otherwise".
|
|
235
|
+
*
|
|
236
|
+
* A constant is a worse affinity key than a real per-session id and a far better one than none:
|
|
237
|
+
* it still gives the upstream one consistent key per install. That is the same trade the sibling
|
|
238
|
+
* `dsh-serenity-hooks` plugin made for the same endpoint, proved on a real host.
|
|
239
|
+
*/
|
|
240
|
+
const OPENCODE_SESSION_FALLBACK = "dsh-any-deepseek";
|
|
241
|
+
/**
|
|
233
242
|
* Presets keyed by a stable id.
|
|
234
243
|
*
|
|
235
244
|
* The id is what a channel entry stores (`preset: opencode-go`), so it is part of the user's
|
|
@@ -493,12 +502,42 @@ function resolveChannel(id, raw) {
|
|
|
493
502
|
input,
|
|
494
503
|
...(entry.reasoningEfforts ?? preset?.reasoningEfforts) === void 0 ? {} : { reasoningEfforts: entry.reasoningEfforts ?? preset?.reasoningEfforts },
|
|
495
504
|
...(entry.compat ?? preset?.compat) === void 0 ? {} : { compat: entry.compat ?? preset?.compat },
|
|
496
|
-
|
|
505
|
+
/**
|
|
506
|
+
* π΄ Headers MERGE per key; they do not replace.
|
|
507
|
+
*
|
|
508
|
+
* `entry.headers ?? preset?.headers` treated the two as alternatives, so a user who added
|
|
509
|
+
* one header of their own silently lost the preset's β and for OpenCode Go those are the
|
|
510
|
+
* headers the endpoint refuses requests without. Merging keeps the preset's required set
|
|
511
|
+
* and lets the entry win only on the keys it actually names.
|
|
512
|
+
*/
|
|
513
|
+
...mergeHeaders(preset?.headers, entry.headers) === void 0 ? {} : { headers: mergeHeaders(preset?.headers, entry.headers) },
|
|
497
514
|
warnings
|
|
498
515
|
}
|
|
499
516
|
};
|
|
500
517
|
}
|
|
501
518
|
/**
|
|
519
|
+
* Merge preset headers with entry headers, the entry winning per key.
|
|
520
|
+
*
|
|
521
|
+
* HTTP field names are case-insensitive, so a user writing `X-Session-ID` must override a
|
|
522
|
+
* preset's `x-session-id` rather than appearing alongside it as a second header β sending both
|
|
523
|
+
* would leave the upstream to pick one, which is exactly the ambiguity the header exists to
|
|
524
|
+
* resolve.
|
|
525
|
+
* @param preset - headers the preset declares.
|
|
526
|
+
* @param entry - headers the entry declares.
|
|
527
|
+
* @returns the merged set, or undefined when neither side declares any.
|
|
528
|
+
*/
|
|
529
|
+
function mergeHeaders(preset, entry) {
|
|
530
|
+
if (entry === void 0) return preset === void 0 ? void 0 : { ...preset };
|
|
531
|
+
if (preset === void 0) return { ...entry };
|
|
532
|
+
const merged = {};
|
|
533
|
+
for (const [name, value] of Object.entries(preset)) merged[name] = value;
|
|
534
|
+
for (const [name, value] of Object.entries(entry)) {
|
|
535
|
+
for (const existing of Object.keys(merged)) if (existing !== name && existing.toLowerCase() === name.toLowerCase()) delete merged[existing];
|
|
536
|
+
merged[name] = value;
|
|
537
|
+
}
|
|
538
|
+
return merged;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
502
541
|
* Resolve every channel in one configuration document.
|
|
503
542
|
*
|
|
504
543
|
* Problems are collected, never thrown: one broken channel must not make the rest of the
|
|
@@ -615,6 +654,29 @@ const PROVIDER_ID = "any-deepseek";
|
|
|
615
654
|
/** Display name for the route in provider selectors. */
|
|
616
655
|
const PROVIDER_NAME = "Any DeepSeek";
|
|
617
656
|
/**
|
|
657
|
+
* Derive the value for the OpenCode session header.
|
|
658
|
+
*
|
|
659
|
+
* π΄ This is THIS PLUGIN's job, not the harness's. `/zen/go` is one relay's private requirement;
|
|
660
|
+
* the host cannot know about it and must not be expected to stamp anything for it. Whatever the
|
|
661
|
+
* harness happens to put on a request is only an input to a decision made here.
|
|
662
|
+
*
|
|
663
|
+
* The id is a stable hash of the harness session id, so:
|
|
664
|
+
* Β· the same conversation keeps one affinity key (what the endpoint wants), and
|
|
665
|
+
* Β· the value is opaque rather than echoing an internal id to a third party.
|
|
666
|
+
*
|
|
667
|
+
* Two hard requirements, both learned from a 400:
|
|
668
|
+
* Β· **never absent** β the header is always sent, so a request can never go out without it;
|
|
669
|
+
* Β· **stable when there is no session** β falling back to a random value would give every
|
|
670
|
+
* request a different affinity key, which is worse than a constant.
|
|
671
|
+
* @param sessionId - the harness session id, when it supplied one.
|
|
672
|
+
* @returns a stable, non-empty session id.
|
|
673
|
+
*/
|
|
674
|
+
function opencodeSessionId(sessionId) {
|
|
675
|
+
const raw = sessionId === void 0 || sessionId === null ? "" : String(sessionId).trim();
|
|
676
|
+
if (raw === "") return OPENCODE_SESSION_FALLBACK;
|
|
677
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
618
680
|
* Build the pi-ai model entry for one channel.
|
|
619
681
|
*
|
|
620
682
|
* The advertised id (`deepseek-v4.1-flash`) never reaches the wire here: each channel's own
|
|
@@ -647,14 +709,57 @@ function buildPiModel(channel) {
|
|
|
647
709
|
};
|
|
648
710
|
}
|
|
649
711
|
/**
|
|
712
|
+
* Collect the bytes for every image in a request.
|
|
713
|
+
*
|
|
714
|
+
* π΄ A separate pass, because the conversion itself is synchronous and the store is not. This is
|
|
715
|
+
* also how the host's own pi-ai adapter does it, for the same reason.
|
|
716
|
+
*
|
|
717
|
+
* A block that cannot be read is left OUT of the map, and the converter then substitutes a text
|
|
718
|
+
* placeholder. That is deliberate: dropping the block would tell the model nothing, while a
|
|
719
|
+
* placeholder at least says an image was attached and names why it could not be shown. Skipping
|
|
720
|
+
* it silently is the failure mode this whole change exists to remove β the user sent an image and
|
|
721
|
+
* the model answered as though the message had been text only.
|
|
722
|
+
* @param options - the harness request.
|
|
723
|
+
* @param readImage - the attachment seam, when the composition has one.
|
|
724
|
+
* @returns resolved images keyed by attachment id.
|
|
725
|
+
*/
|
|
726
|
+
async function collectImages(options, readImage) {
|
|
727
|
+
const images = /* @__PURE__ */ new Map();
|
|
728
|
+
if (readImage === void 0) return images;
|
|
729
|
+
const pending = [];
|
|
730
|
+
for (const message of options.messages) {
|
|
731
|
+
if (!Array.isArray(message.content)) continue;
|
|
732
|
+
for (const block of message.content) {
|
|
733
|
+
const candidate = block;
|
|
734
|
+
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
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const resolved = await Promise.all(pending.map(async (item) => {
|
|
746
|
+
try {
|
|
747
|
+
return [item.id, await readImage(item.id, item.mediaType)];
|
|
748
|
+
} catch {
|
|
749
|
+
return [item.id, void 0];
|
|
750
|
+
}
|
|
751
|
+
}));
|
|
752
|
+
for (const [id, value] of resolved) if (value !== void 0) images.set(id, value);
|
|
753
|
+
return images;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
650
756
|
* Translate one harness request into a pi-ai context.
|
|
651
757
|
*
|
|
652
|
-
* The harness already projects images and files before an adapter sees them, so this is a
|
|
653
|
-
* straightforward content mapping; anything the harness did not project is passed through.
|
|
654
758
|
* @param options - the harness request.
|
|
759
|
+
* @param images - image bytes collected by {@link collectImages}; omitted for a text-only request.
|
|
655
760
|
* @returns a pi-ai context plus the one-shot system prompt, when the caller set one.
|
|
656
761
|
*/
|
|
657
|
-
function toPiContext(options) {
|
|
762
|
+
function toPiContext(options, images) {
|
|
658
763
|
const messages = options.messages.map((message) => {
|
|
659
764
|
if (message.role === "assistant") return {
|
|
660
765
|
role: "assistant",
|
|
@@ -691,7 +796,7 @@ function toPiContext(options) {
|
|
|
691
796
|
};
|
|
692
797
|
return {
|
|
693
798
|
role: "user",
|
|
694
|
-
content:
|
|
799
|
+
content: userContent(message.content, images),
|
|
695
800
|
timestamp: 0
|
|
696
801
|
};
|
|
697
802
|
});
|
|
@@ -731,14 +836,45 @@ function safeJson(raw) {
|
|
|
731
836
|
return {};
|
|
732
837
|
}
|
|
733
838
|
}
|
|
734
|
-
/**
|
|
735
|
-
|
|
839
|
+
/**
|
|
840
|
+
* Map one user message's blocks to pi-ai content.
|
|
841
|
+
*
|
|
842
|
+
* π΄ An image block carries an `attachmentId`, never bytes β so it can only become an image when
|
|
843
|
+
* {@link collectImages} resolved it. When it did not, the block becomes a TEXT PLACEHOLDER rather
|
|
844
|
+
* than disappearing: the model should be told an image was attached and that it could not be
|
|
845
|
+
* read. The previous version dropped such blocks entirely, which is how a user's screenshot
|
|
846
|
+
* reached the plugin and the model answered as though the message had been text only.
|
|
847
|
+
* @param content - harness content blocks.
|
|
848
|
+
* @param images - resolved image bytes, when any were collected.
|
|
849
|
+
* @returns pi-ai content blocks.
|
|
850
|
+
*/
|
|
851
|
+
function userContent(content, images) {
|
|
736
852
|
const out = [];
|
|
737
853
|
for (const block of content) {
|
|
738
854
|
const typed = block;
|
|
739
|
-
if (typed.type === "text" && typeof typed.text === "string")
|
|
855
|
+
if (typed.type === "text" && typeof typed.text === "string") {
|
|
856
|
+
out.push({
|
|
857
|
+
type: "text",
|
|
858
|
+
text: typed.text
|
|
859
|
+
});
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (typed.type !== "image") continue;
|
|
863
|
+
const id = typed.attachment?.attachmentId;
|
|
864
|
+
const resolved = typeof id === "string" ? images?.get(id) : void 0;
|
|
865
|
+
if (resolved !== void 0) {
|
|
866
|
+
out.push({
|
|
867
|
+
type: "image",
|
|
868
|
+
data: resolved.data,
|
|
869
|
+
mimeType: resolved.mediaType
|
|
870
|
+
});
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (typed.offloaded === true || typed.attachment?.offloaded === true) continue;
|
|
874
|
+
const name = typed.attachment?.name === void 0 ? "" : ` (${typed.attachment.name})`;
|
|
875
|
+
out.push({
|
|
740
876
|
type: "text",
|
|
741
|
-
text:
|
|
877
|
+
text: `[an image was attached${name}, but this route could not read its bytes]`
|
|
742
878
|
});
|
|
743
879
|
}
|
|
744
880
|
return out;
|
|
@@ -1125,20 +1261,20 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1125
1261
|
*
|
|
1126
1262
|
* Two things OpenCode Go requires cannot live in a preset, because neither is a constant:
|
|
1127
1263
|
*
|
|
1128
|
-
* Β· `x-opencode-session` must be STABLE PER CONVERSATION
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1264
|
+
* Β· `x-opencode-session` must be STABLE PER CONVERSATION, and it must ALWAYS be present.
|
|
1265
|
+
* Both halves matter, and getting only the first produced a 400: an earlier version read
|
|
1266
|
+
* `options.sessionId` and sent nothing when it was absent β and the harness does not stamp
|
|
1267
|
+
* one on every request. So the value is a stable hash of the harness session id when we
|
|
1268
|
+
* have one, and a constant when we do not. A hash keeps the id opaque while staying stable
|
|
1269
|
+
* for the same conversation, which is all the endpoint's affinity key needs.
|
|
1132
1270
|
* Β· `User-Agent` must identify THIS client rather than the generic SDK. pi-ai sets its own,
|
|
1133
1271
|
* so the preset's static headers alone can never satisfy that rule.
|
|
1134
1272
|
*/
|
|
1135
1273
|
const requestHeaders = {};
|
|
1136
1274
|
if (channel.headers !== void 0 && Object.hasOwn(channel.headers, "x-opencode-client")) {
|
|
1137
|
-
const session =
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
requestHeaders["X-Session-ID"] = session;
|
|
1141
|
-
}
|
|
1275
|
+
const session = opencodeSessionId(options.sessionId);
|
|
1276
|
+
requestHeaders[OPENCODE_SESSION_HEADER] = session;
|
|
1277
|
+
requestHeaders["X-Session-ID"] = session;
|
|
1142
1278
|
requestHeaders["User-Agent"] = CLIENT_USER_AGENT;
|
|
1143
1279
|
}
|
|
1144
1280
|
const streamOptions = {
|
|
@@ -1150,7 +1286,8 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
1150
1286
|
...Object.keys(requestHeaders).length === 0 ? {} : { headers: requestHeaders }
|
|
1151
1287
|
};
|
|
1152
1288
|
try {
|
|
1153
|
-
const
|
|
1289
|
+
const images = await collectImages(options, this.deps.readImage);
|
|
1290
|
+
const events = models.streamSimple(model, toPiContext(options, images), streamOptions);
|
|
1154
1291
|
for await (const chunk of toStreamChunks(events, (observed) => {
|
|
1155
1292
|
outcome = observed;
|
|
1156
1293
|
})) yield chunk;
|
|
@@ -1757,10 +1894,45 @@ function apply(ctx, config) {
|
|
|
1757
1894
|
const value = unwrap((ctx.fiber?.config ?? config).current);
|
|
1758
1895
|
return typeof value === "string" ? value : void 0;
|
|
1759
1896
|
};
|
|
1897
|
+
/**
|
|
1898
|
+
* Read one image's request bytes from the harness attachment store.
|
|
1899
|
+
*
|
|
1900
|
+
* π΄ This is the host seam, not something this plugin invents: a harness `ImageBlock` carries
|
|
1901
|
+
* only an `attachmentId`, and `readImageRequest` is the documented way to turn that into bytes.
|
|
1902
|
+
* Without this path the model cannot receive an image no matter what `input` declares β which
|
|
1903
|
+
* was exactly this plugin's state until now, and why a screenshot the user sent never arrived.
|
|
1904
|
+
*
|
|
1905
|
+
* The target bounds are the store's own normalization request, not a guess about the relay:
|
|
1906
|
+
* 2048px keeps the harness's normal form, and the byte ceiling is the one the host's pi-ai
|
|
1907
|
+
* adapter uses for the same purpose.
|
|
1908
|
+
*/
|
|
1909
|
+
const readImage = async (attachmentId, mediaType) => {
|
|
1910
|
+
const attachments = service(ctx, "attachments");
|
|
1911
|
+
if (attachments?.readImageRequest === void 0) return void 0;
|
|
1912
|
+
try {
|
|
1913
|
+
const stored = await attachments.readImageRequest({
|
|
1914
|
+
attachmentId,
|
|
1915
|
+
mediaType
|
|
1916
|
+
}, {
|
|
1917
|
+
width: 2048,
|
|
1918
|
+
height: 2048,
|
|
1919
|
+
maxBytes: 4194304
|
|
1920
|
+
});
|
|
1921
|
+
if (stored?.data === void 0) return void 0;
|
|
1922
|
+
return {
|
|
1923
|
+
data: Buffer.from(stored.data).toString("base64"),
|
|
1924
|
+
mediaType: stored.mediaType ?? mediaType
|
|
1925
|
+
};
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
console.warn(`[any-deepseek] θ―»δΈε°εΎη ${attachmentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1760
1931
|
const adapter = new AnyDeepSeekAdapter({
|
|
1761
1932
|
channels,
|
|
1762
1933
|
current,
|
|
1763
1934
|
resolveKey,
|
|
1935
|
+
readImage,
|
|
1764
1936
|
record: makeRecorder(ctx, config),
|
|
1765
1937
|
now: () => /* @__PURE__ */ new Date(),
|
|
1766
1938
|
elapsed: () => Number(process.hrtime.bigint() / 1000000n)
|
package/lib/presets.d.ts
CHANGED
|
@@ -84,11 +84,27 @@ export declare const DEFAULT_MAX_TOKENS = 32768;
|
|
|
84
84
|
/**
|
|
85
85
|
* The user agent a request to OpenCode Go identifies itself with.
|
|
86
86
|
*
|
|
87
|
-
*
|
|
87
|
+
* The upstream asks clients to identify themselves rather than send a generic SDK name.
|
|
88
88
|
*/
|
|
89
89
|
export declare const CLIENT_USER_AGENT = "dsh-any-deepseek/1.0";
|
|
90
90
|
/** Header carrying the per-conversation routing id on OpenCode Go. */
|
|
91
91
|
export declare const OPENCODE_SESSION_HEADER = "x-opencode-session";
|
|
92
|
+
/**
|
|
93
|
+
* Session id used when the harness gives us none.
|
|
94
|
+
*
|
|
95
|
+
* π΄ **This fallback is load-bearing, not decoration**, and dropping it is a measured failure.
|
|
96
|
+
*
|
|
97
|
+
* The endpoint wants a per-conversation id, so the honest implementation is "use the harness's
|
|
98
|
+
* `sessionId`". The problem is that the harness does not stamp one on every request β and when it
|
|
99
|
+
* does not, an implementation that ONLY reads `options.sessionId` sends **no session header at
|
|
100
|
+
* all**, which the endpoint rejects with 400 `MissingSessionID`: precisely the failure the header
|
|
101
|
+
* exists to prevent. So the value is "harness session when present, this constant otherwise".
|
|
102
|
+
*
|
|
103
|
+
* A constant is a worse affinity key than a real per-session id and a far better one than none:
|
|
104
|
+
* it still gives the upstream one consistent key per install. That is the same trade the sibling
|
|
105
|
+
* `dsh-serenity-hooks` plugin made for the same endpoint, proved on a real host.
|
|
106
|
+
*/
|
|
107
|
+
export declare const OPENCODE_SESSION_FALLBACK = "dsh-any-deepseek";
|
|
92
108
|
/**
|
|
93
109
|
* Presets keyed by a stable id.
|
|
94
110
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-deepseek",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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": {
|