@adaptiqai/deepseek-harness 0.2.12 → 0.2.13
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/README.md +1 -0
- package/lib/client.js +2 -2
- package/lib/host.js +227 -32
- package/lib/types/host/adapter.d.ts +15 -0
- package/lib/types/host/images.d.ts +91 -0
- package/lib/types/host/wire.d.ts +25 -7
- package/package.json +10 -10
package/lib/host.js
CHANGED
|
@@ -54,7 +54,7 @@ import { attributionHeaders as attributionHeaders2 } from "@deepseek-ai/dsh-llm"
|
|
|
54
54
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
55
55
|
|
|
56
56
|
// src/host/adapter.ts
|
|
57
|
-
import { attributionHeaders, LlmAdapter, LlmError, ProviderRequestId } from "@deepseek-ai/dsh-llm";
|
|
57
|
+
import { attributionHeaders, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ProviderRequestId } from "@deepseek-ai/dsh-llm";
|
|
58
58
|
|
|
59
59
|
// ../core/lib/errors.js
|
|
60
60
|
var CODES = {
|
|
@@ -122,21 +122,26 @@ function classifyFailure(status, raw, headers, model2, options = {}) {
|
|
|
122
122
|
const library = /\blibrary is full/iu.test(detail.message ?? "");
|
|
123
123
|
return { ...make(CODES.memoryFull, library ? MEMORY_FULL_LIBRARY : MEMORY_FULL, "memory-full"), memoryFull: library ? "library" : "no-room" };
|
|
124
124
|
}
|
|
125
|
+
if (detail.code === "plan_usage_exhausted") {
|
|
126
|
+
const renewsAt = envelope.detail?.renews_at;
|
|
127
|
+
const failure = make(CODES.noLanes, detail.message ?? "Your plan's usage is used up for now. Rent extra lanes in the meantime.", "no-lanes");
|
|
128
|
+
return typeof renewsAt === "number" ? { ...failure, planRenewsAt: renewsAt } : failure;
|
|
129
|
+
}
|
|
125
130
|
if (effective === 402 || detail.code === "rental_paused") {
|
|
126
131
|
const releaseAt = envelope.detail?.release_at;
|
|
127
132
|
const minutes = typeof releaseAt === "number" ? Math.max(1, Math.ceil((releaseAt * 1e3 - Date.now()) / 6e4)) : null;
|
|
128
133
|
const keep = minutes === null ? "to continue" : `within ${minutes} minute${minutes === 1 ? "" : "s"} to keep them`;
|
|
129
|
-
const failure = make(CODES.outOfCredit, `Your balance ran out, so your lanes
|
|
134
|
+
const failure = make(CODES.outOfCredit, `Your balance ran out, so your lanes are paused. Add funds ${keep}.`, "out-of-credit");
|
|
130
135
|
return minutes === null ? failure : { ...failure, minutesLeft: minutes };
|
|
131
136
|
}
|
|
132
137
|
if (effective === 401) {
|
|
133
138
|
return make(CODES.signIn, "Your AdaptiqAI sign-in has expired. Open Settings \u2192 AdaptiqAI and sign in again.", "sign-in");
|
|
134
139
|
}
|
|
135
140
|
if (detail.code === "rental_stopped_unused") {
|
|
136
|
-
return make(CODES.noLanes,
|
|
141
|
+
return make(CODES.noLanes, "Your lane stopped because it went unused, so nothing more was charged. Get access to start it again.", "no-lanes");
|
|
137
142
|
}
|
|
138
143
|
if (has(detail, "AdaptiqLaneAccessRequired", "inference_entitlement_required", "instance_access_required", "lane_access") || effective === 403 && has(detail, "lane", "entitle", "model access")) {
|
|
139
|
-
return make(CODES.noLanes,
|
|
144
|
+
return make(CODES.noLanes, "You need to rent a lane or subscribe to AdaptiqAI to continue.", "no-lanes");
|
|
140
145
|
}
|
|
141
146
|
if (effective === 403) {
|
|
142
147
|
return make(CODES.signIn, "This AdaptiqAI connection isn't allowed to do that. Sign in again from Settings \u2192 AdaptiqAI.", "sign-in");
|
|
@@ -321,10 +326,10 @@ var REFRESH_MARGIN_MS = 2 * 6e4;
|
|
|
321
326
|
var DEVICE_TIMEOUT_MS = 10 * 6e4;
|
|
322
327
|
var UNREACHABLE = "AdaptiqAI can't be reached. Check your internet connection and try again.";
|
|
323
328
|
function deviceLabel(value) {
|
|
324
|
-
let
|
|
325
|
-
while (Buffer.byteLength(
|
|
326
|
-
|
|
327
|
-
return
|
|
329
|
+
let label2 = value.replace(/\.local$/iu, "").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/gu, " ").trim() || "this computer";
|
|
330
|
+
while (Buffer.byteLength(label2) > 80)
|
|
331
|
+
label2 = label2.slice(0, -1);
|
|
332
|
+
return label2;
|
|
328
333
|
}
|
|
329
334
|
var defaultSleep = (ms, signal) => new Promise((resolve2, reject) => {
|
|
330
335
|
if (signal.aborted) {
|
|
@@ -1274,15 +1279,19 @@ var en = {
|
|
|
1274
1279
|
n_connected_body: "Your AdaptiqAI models are in the model picker.",
|
|
1275
1280
|
n_compaction_title: "Nothing to compact",
|
|
1276
1281
|
n_compaction_body: "AdaptiqAI keeps the whole conversation in memory, so nothing needs shortening.",
|
|
1282
|
+
n_imagesTextOnly_title: "Images were not sent",
|
|
1283
|
+
n_imagesTextOnly_body: "{model} reads text only, so the model got a short note in place of each image. Choose a model that reads images to ask about them.",
|
|
1277
1284
|
n_signIn_title: "Sign in to keep going",
|
|
1278
1285
|
n_signIn_body: "Your AdaptiqAI connection needs a fresh sign-in.",
|
|
1279
1286
|
n_connect_body: "Connect your AdaptiqAI account to use this model.",
|
|
1280
|
-
n_noLanes_title: "
|
|
1281
|
-
n_noLanes_body: "
|
|
1282
|
-
n_laneStopped_body: "Your lane
|
|
1287
|
+
n_noLanes_title: "Rent a lane to continue",
|
|
1288
|
+
n_noLanes_body: "You need to rent a lane or subscribe to AdaptiqAI to continue.",
|
|
1289
|
+
n_laneStopped_body: "Your lane stopped because it went unused, so nothing more was charged.",
|
|
1290
|
+
n_planUsedUp_body: "Your plan's usage is used up until {until}. Rent extra lanes in the meantime.",
|
|
1291
|
+
n_planUsedUpNoDate_body: "Your plan's usage is used up for now. Rent extra lanes in the meantime.",
|
|
1283
1292
|
n_credit_title: "Add funds to continue",
|
|
1284
|
-
n_credit_body: "Your balance ran out, so your lanes
|
|
1285
|
-
n_creditMinutes_body: "Your balance ran out, so your lanes
|
|
1293
|
+
n_credit_body: "Your balance ran out, so your lanes are paused.",
|
|
1294
|
+
n_creditMinutes_body: "Your balance ran out, so your lanes are paused. Add funds within {minutes} min to keep them.",
|
|
1286
1295
|
n_busy_title: "All lanes are busy",
|
|
1287
1296
|
n_busy_body: "Your request on {model} is waiting for a free lane and will retry by itself.",
|
|
1288
1297
|
n_preparing_title: "Getting your conversation ready",
|
|
@@ -1674,8 +1683,8 @@ var str4 = (value) => typeof value === "string" && value.trim() !== "" ? value.t
|
|
|
1674
1683
|
function say(title, body, params = {}) {
|
|
1675
1684
|
return { title: fill(en[title], params), body: body === null ? "" : fill(en[body], params), text: { title, body, params } };
|
|
1676
1685
|
}
|
|
1677
|
-
function button(id,
|
|
1678
|
-
return { id, label: en[
|
|
1686
|
+
function button(id, label2, extra = {}) {
|
|
1687
|
+
return { id, label: en[label2], labelKey: label2, ...extra };
|
|
1679
1688
|
}
|
|
1680
1689
|
var count2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1681
1690
|
var PRIVATE_HELD = "A private conversation is not kept on AdaptiqAI, so it can't use this before it exists there. Turn Remember on for this conversation to add it.";
|
|
@@ -1842,7 +1851,7 @@ function copyFailure(error, model2) {
|
|
|
1842
1851
|
case "model_mismatch":
|
|
1843
1852
|
return str4(detail.message) ?? `This collection can't be made available for ${model2}.`;
|
|
1844
1853
|
case "inference_entitlement_required":
|
|
1845
|
-
return
|
|
1854
|
+
return "You need to rent a lane or subscribe to AdaptiqAI to continue.";
|
|
1846
1855
|
case "collection_not_found":
|
|
1847
1856
|
return "This collection no longer exists.";
|
|
1848
1857
|
case "sources_not_exportable":
|
|
@@ -2733,13 +2742,13 @@ var Workspace = class {
|
|
|
2733
2742
|
this.publish();
|
|
2734
2743
|
}
|
|
2735
2744
|
/** A browser approval completed. */
|
|
2736
|
-
signedIn(
|
|
2745
|
+
signedIn(label2) {
|
|
2737
2746
|
if (this.state.defaultModel === void 0) {
|
|
2738
2747
|
this.state = { ...this.state, defaultModel: "pending" };
|
|
2739
2748
|
void this.persist();
|
|
2740
2749
|
}
|
|
2741
2750
|
this.clearNotices("sign-in");
|
|
2742
|
-
this.raise({ kind: "connected", tone: "success", ...say("connectedTo", "n_connected_body", { workspace:
|
|
2751
|
+
this.raise({ kind: "connected", tone: "success", ...say("connectedTo", "n_connected_body", { workspace: label2 }), actions: [] });
|
|
2743
2752
|
this.summaryMode = "unknown";
|
|
2744
2753
|
this.summaryMissingAt = null;
|
|
2745
2754
|
void this.refresh();
|
|
@@ -3325,6 +3334,14 @@ var Workspace = class {
|
|
|
3325
3334
|
actions: [button("open-console", "n_openConsole", { url: this.links.console, primary: true })]
|
|
3326
3335
|
});
|
|
3327
3336
|
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Say that a message's images were not sent because the model reads text only.
|
|
3339
|
+
* @param sessionId - the conversation, when known.
|
|
3340
|
+
* @param modelName - the model's display name.
|
|
3341
|
+
*/
|
|
3342
|
+
imagesNotSent(sessionId2, modelName) {
|
|
3343
|
+
this.raise({ kind: "images", tone: "warning", ...say("n_imagesTextOnly_title", "n_imagesTextOnly_body", { model: modelName }), actions: [], ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 } });
|
|
3344
|
+
}
|
|
3328
3345
|
failed(sessionId2, model2, failure) {
|
|
3329
3346
|
if (failure.code === CODES.compaction) {
|
|
3330
3347
|
this.raise({ kind: "compaction", tone: "info", ...say("n_compaction_title", "n_compaction_body"), actions: [], ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 } });
|
|
@@ -3366,11 +3383,13 @@ var Workspace = class {
|
|
|
3366
3383
|
break;
|
|
3367
3384
|
case "no-lanes": {
|
|
3368
3385
|
const offer = this.offers.find((item) => item.model === model2) ?? this.offers.find((item) => item.name.toLowerCase() === name2.toLowerCase());
|
|
3386
|
+
const body = failure.serverCode === "rental_stopped_unused" ? "n_laneStopped_body" : failure.planRenewsAt !== void 0 ? "n_planUsedUp_body" : failure.serverCode === "plan_usage_exhausted" ? "n_planUsedUpNoDate_body" : "n_noLanes_body";
|
|
3387
|
+
const until = failure.planRenewsAt === void 0 ? "" : `${new Date(failure.planRenewsAt * 1e3).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
3369
3388
|
this.raise({
|
|
3370
3389
|
kind: "no-lanes",
|
|
3371
3390
|
tone: "warning",
|
|
3372
3391
|
modal: true,
|
|
3373
|
-
...say("n_noLanes_title",
|
|
3392
|
+
...say("n_noLanes_title", body, { until }),
|
|
3374
3393
|
actions: [button("get-access", "getAccess", { primary: true, ...offer === void 0 ? {} : { offerId: offer.id } }), button("dismiss", "notNow")]
|
|
3375
3394
|
});
|
|
3376
3395
|
break;
|
|
@@ -3380,7 +3399,7 @@ var Workspace = class {
|
|
|
3380
3399
|
kind: "out-of-credit",
|
|
3381
3400
|
tone: "error",
|
|
3382
3401
|
modal: true,
|
|
3383
|
-
...failure.minutesLeft === void 0 ? say("n_credit_title", "n_credit_body"
|
|
3402
|
+
...failure.minutesLeft === void 0 ? say("n_credit_title", "n_credit_body") : say("n_credit_title", "n_creditMinutes_body", { minutes: String(failure.minutesLeft) }),
|
|
3384
3403
|
actions: [button("add-funds", "n_addFunds", { url: this.links.models, primary: true }), button("dismiss", "notNow")]
|
|
3385
3404
|
});
|
|
3386
3405
|
break;
|
|
@@ -3560,7 +3579,7 @@ var Workspace = class {
|
|
|
3560
3579
|
if (target === void 0)
|
|
3561
3580
|
throw new Error("Choose an AdaptiqAI model first.");
|
|
3562
3581
|
const modelName = this.modelName(target);
|
|
3563
|
-
const
|
|
3582
|
+
const label2 = modelName ?? target;
|
|
3564
3583
|
let body;
|
|
3565
3584
|
try {
|
|
3566
3585
|
body = await this.deps.api.json(`/v1/memories/${encodeURIComponent(id)}/copies`, {
|
|
@@ -3576,7 +3595,7 @@ var Workspace = class {
|
|
|
3576
3595
|
this.copies.set(copyKey(id, target), { source: id, from, target, copy: null, status: "building" });
|
|
3577
3596
|
return { status: "building", model: target, modelName };
|
|
3578
3597
|
}
|
|
3579
|
-
throw new Error(copyFailure(error,
|
|
3598
|
+
throw new Error(copyFailure(error, label2));
|
|
3580
3599
|
}
|
|
3581
3600
|
const copy = obj4(body?.copy);
|
|
3582
3601
|
const status = copyStatus(str4(copy?.status));
|
|
@@ -4671,7 +4690,70 @@ async function* translate(events, hooks) {
|
|
|
4671
4690
|
};
|
|
4672
4691
|
}
|
|
4673
4692
|
|
|
4693
|
+
// src/host/images.ts
|
|
4694
|
+
var ENGINE_IMAGE_LIMITS = Object.freeze({
|
|
4695
|
+
maxImages: 8,
|
|
4696
|
+
maxImageBytes: 20 * 1024 * 1024,
|
|
4697
|
+
maxPixels: 16e6
|
|
4698
|
+
});
|
|
4699
|
+
var ImageProblem = class extends Error {
|
|
4700
|
+
name = "ImageProblem";
|
|
4701
|
+
};
|
|
4702
|
+
var startsWith = (data, bytes, at = 0) => data.length >= at + bytes.length && bytes.every((byte, index) => data[at + index] === byte);
|
|
4703
|
+
var ascii = (text2) => [...text2].map((char) => char.charCodeAt(0));
|
|
4704
|
+
function sniffImageType(data) {
|
|
4705
|
+
if (startsWith(data, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
|
|
4706
|
+
if (startsWith(data, [255, 216, 255])) return "image/jpeg";
|
|
4707
|
+
if (startsWith(data, ascii("GIF87a")) || startsWith(data, ascii("GIF89a"))) return "image/gif";
|
|
4708
|
+
if (startsWith(data, ascii("RIFF")) && startsWith(data, ascii("WEBP"), 8)) return "image/webp";
|
|
4709
|
+
return null;
|
|
4710
|
+
}
|
|
4711
|
+
function fitPixels(width, height, maxPixels) {
|
|
4712
|
+
if (width * height <= maxPixels) return { width, height };
|
|
4713
|
+
const scale = Math.sqrt(maxPixels / (width * height));
|
|
4714
|
+
let w = Math.max(1, Math.floor(width * scale));
|
|
4715
|
+
let h = Math.max(1, Math.floor(height * scale));
|
|
4716
|
+
while (w * h > maxPixels) {
|
|
4717
|
+
if (w >= h) w--;
|
|
4718
|
+
else h--;
|
|
4719
|
+
}
|
|
4720
|
+
return { width: w, height: h };
|
|
4721
|
+
}
|
|
4722
|
+
var carriesImages = (message) => message.role === "user" || message.role === "tool";
|
|
4723
|
+
function sentImages(messages) {
|
|
4724
|
+
const out = [];
|
|
4725
|
+
for (const message of messages) {
|
|
4726
|
+
if (!carriesImages(message)) continue;
|
|
4727
|
+
for (const block of message.content) if (block.type === "image" && block.offloaded !== true) out.push(block);
|
|
4728
|
+
}
|
|
4729
|
+
return out;
|
|
4730
|
+
}
|
|
4731
|
+
function hasImages(messages) {
|
|
4732
|
+
return messages.some((message) => message.content.some((block) => block.type === "image"));
|
|
4733
|
+
}
|
|
4734
|
+
var mib = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} MB`;
|
|
4735
|
+
var label = (ref) => ref.name === void 0 ? "An image" : `The image "${ref.name}"`;
|
|
4736
|
+
async function prepareImages(messages, source, limits, signal) {
|
|
4737
|
+
const refs = /* @__PURE__ */ new Map();
|
|
4738
|
+
for (const block of sentImages(messages)) refs.set(String(block.attachment.attachmentId), block.attachment);
|
|
4739
|
+
const entries = await Promise.all([...refs].map(async ([id, ref]) => {
|
|
4740
|
+
const target = { ...fitPixels(ref.width, ref.height, limits.maxPixels), maxBytes: limits.maxImageBytes };
|
|
4741
|
+
const version = await source.readImageRequest(ref, target, signal);
|
|
4742
|
+
const mediaType = sniffImageType(version.data);
|
|
4743
|
+
if (mediaType === null) throw new ImageProblem(`${label(ref)} is not a PNG, JPEG, WebP or GIF file, so AdaptiqAI can't read it. Attach it in one of those formats.`);
|
|
4744
|
+
if (version.data.byteLength > limits.maxImageBytes) {
|
|
4745
|
+
throw new ImageProblem(`${label(ref)} is ${mib(version.data.byteLength)}; AdaptiqAI takes images up to ${mib(limits.maxImageBytes)}. Attach a smaller copy.`);
|
|
4746
|
+
}
|
|
4747
|
+
if (version.width * version.height > limits.maxPixels) {
|
|
4748
|
+
throw new ImageProblem(`${label(ref)} is ${version.width}\xD7${version.height} pixels, more than AdaptiqAI takes. Attach a smaller copy.`);
|
|
4749
|
+
}
|
|
4750
|
+
return [id, { url: `data:${mediaType};base64,${Buffer.from(version.data).toString("base64")}`, width: version.width, height: version.height }];
|
|
4751
|
+
}));
|
|
4752
|
+
return new Map(entries);
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4674
4755
|
// src/host/wire.ts
|
|
4756
|
+
import { offloadedImageText, requestImageHandleText, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
4675
4757
|
function sessionReferences(message) {
|
|
4676
4758
|
if (message.role !== "user") return null;
|
|
4677
4759
|
const source = message.source;
|
|
@@ -4763,16 +4845,62 @@ function userTurns(messages) {
|
|
|
4763
4845
|
}
|
|
4764
4846
|
var text = (blocks) => blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
4765
4847
|
var reasoning = (blocks) => blocks.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
4848
|
+
var Parts = class {
|
|
4849
|
+
parts = [];
|
|
4850
|
+
glue = false;
|
|
4851
|
+
text(value, own = false) {
|
|
4852
|
+
if (value === "") return;
|
|
4853
|
+
const last = this.parts.at(-1);
|
|
4854
|
+
if (last?.type === "text") last.text += (own || this.glue ? "\n" : "") + value;
|
|
4855
|
+
else this.parts.push({ type: "text", text: value });
|
|
4856
|
+
this.glue = own;
|
|
4857
|
+
}
|
|
4858
|
+
image(url) {
|
|
4859
|
+
this.parts.push({ type: "image_url", image_url: { url } });
|
|
4860
|
+
this.glue = false;
|
|
4861
|
+
}
|
|
4862
|
+
get hasImage() {
|
|
4863
|
+
return this.parts.some((part) => part.type === "image_url");
|
|
4864
|
+
}
|
|
4865
|
+
get joined() {
|
|
4866
|
+
return this.parts.map((part) => part.type === "text" ? part.text : "").join("");
|
|
4867
|
+
}
|
|
4868
|
+
};
|
|
4869
|
+
function putImage(block, images, parts, inline) {
|
|
4870
|
+
const access2 = images?.access?.(block.attachment);
|
|
4871
|
+
if (block.offloaded === true) {
|
|
4872
|
+
parts.text(offloadedImageText(block.attachment, access2), true);
|
|
4873
|
+
return null;
|
|
4874
|
+
}
|
|
4875
|
+
const sent = images?.sent?.get(String(block.attachment.attachmentId));
|
|
4876
|
+
if (sent === void 0) {
|
|
4877
|
+
parts.text(textOnlyImageText(block.attachment), true);
|
|
4878
|
+
return null;
|
|
4879
|
+
}
|
|
4880
|
+
parts.text(requestImageHandleText(block.attachment, sent, access2), true);
|
|
4881
|
+
if (!inline) return sent;
|
|
4882
|
+
parts.image(sent.url);
|
|
4883
|
+
return null;
|
|
4884
|
+
}
|
|
4766
4885
|
function replayField(message) {
|
|
4767
4886
|
const source = message.source;
|
|
4768
4887
|
const state2 = source?.replayState;
|
|
4769
4888
|
const field = state2?.response?.reasoningField;
|
|
4770
4889
|
return typeof field === "string" && /^[a-z_]{1,32}$/u.test(field) ? field : "reasoning_content";
|
|
4771
4890
|
}
|
|
4772
|
-
function wireMessages(options, recalled = /* @__PURE__ */ new Map()) {
|
|
4891
|
+
function wireMessages(options, recalled = /* @__PURE__ */ new Map(), images) {
|
|
4773
4892
|
const out = [];
|
|
4774
4893
|
if (options.system !== void 0 && options.system !== "") out.push({ role: "system", content: options.system });
|
|
4894
|
+
let toolImages = [];
|
|
4895
|
+
const flushToolImages = () => {
|
|
4896
|
+
if (toolImages.length === 0) return;
|
|
4897
|
+
const parts = [{ type: "text", text: toolImages.length === 1 ? "The image from the tool result above:" : "The images from the tool results above, in order:" }];
|
|
4898
|
+
for (const image of toolImages) parts.push({ type: "image_url", image_url: { url: image.url } });
|
|
4899
|
+
out.push({ role: "user", content: parts });
|
|
4900
|
+
toolImages = [];
|
|
4901
|
+
};
|
|
4775
4902
|
options.messages.forEach((message, index) => {
|
|
4903
|
+
if (message.role !== "tool") flushToolImages();
|
|
4776
4904
|
switch (message.role) {
|
|
4777
4905
|
case "system": {
|
|
4778
4906
|
const content = text(message.content);
|
|
@@ -4784,6 +4912,15 @@ function wireMessages(options, recalled = /* @__PURE__ */ new Map()) {
|
|
|
4784
4912
|
return;
|
|
4785
4913
|
}
|
|
4786
4914
|
case "user": {
|
|
4915
|
+
if (message.content.some((block) => block.type === "image")) {
|
|
4916
|
+
const parts = new Parts();
|
|
4917
|
+
for (const block of message.content) {
|
|
4918
|
+
if (block.type === "text") parts.text(block.text);
|
|
4919
|
+
else if (block.type === "image") putImage(block, images, parts, true);
|
|
4920
|
+
}
|
|
4921
|
+
out.push({ role: "user", content: parts.hasImage ? parts.parts : parts.joined });
|
|
4922
|
+
return;
|
|
4923
|
+
}
|
|
4787
4924
|
const references = recalled.size === 0 ? null : sessionReferences(message);
|
|
4788
4925
|
const content = text(message.content);
|
|
4789
4926
|
out.push({ role: "user", content: references === null ? content : recallSnapshot(content, references, recalled) });
|
|
@@ -4805,7 +4942,15 @@ function wireMessages(options, recalled = /* @__PURE__ */ new Map()) {
|
|
|
4805
4942
|
return;
|
|
4806
4943
|
}
|
|
4807
4944
|
case "tool": {
|
|
4808
|
-
const
|
|
4945
|
+
const parts = new Parts();
|
|
4946
|
+
for (const block of message.content) {
|
|
4947
|
+
if (block.type === "text") parts.text(block.text);
|
|
4948
|
+
else if (block.type === "image") {
|
|
4949
|
+
const deferred = putImage(block, images, parts, false);
|
|
4950
|
+
if (deferred !== null) toolImages.push(deferred);
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
const content = parts.joined;
|
|
4809
4954
|
out.push({ role: "tool", tool_call_id: message.toolCallId, content: content === "" ? "(no output)" : content });
|
|
4810
4955
|
return;
|
|
4811
4956
|
}
|
|
@@ -4813,12 +4958,13 @@ function wireMessages(options, recalled = /* @__PURE__ */ new Map()) {
|
|
|
4813
4958
|
return;
|
|
4814
4959
|
}
|
|
4815
4960
|
});
|
|
4961
|
+
flushToolImages();
|
|
4816
4962
|
return out;
|
|
4817
4963
|
}
|
|
4818
4964
|
function buildChatBody(options, model2, extras) {
|
|
4819
4965
|
const body = {
|
|
4820
4966
|
model: options.model,
|
|
4821
|
-
messages: wireMessages(options, extras.recalled),
|
|
4967
|
+
messages: wireMessages(options, extras.recalled, extras.images),
|
|
4822
4968
|
stream: true,
|
|
4823
4969
|
stream_options: { include_usage: extras.includeUsage ?? true }
|
|
4824
4970
|
};
|
|
@@ -4970,6 +5116,41 @@ var AdaptiqAdapter = class extends LlmAdapter {
|
|
|
4970
5116
|
stream(options) {
|
|
4971
5117
|
return this.generate(options);
|
|
4972
5118
|
}
|
|
5119
|
+
/**
|
|
5120
|
+
* The request's images, read from the harness's stored copies, when the
|
|
5121
|
+
* model's card says it reads images. Otherwise each image goes as a short
|
|
5122
|
+
* note and the person is told.
|
|
5123
|
+
*/
|
|
5124
|
+
async images(options, model2, sessionId2, fail2, signal) {
|
|
5125
|
+
if (!hasImages(options.messages)) return void 0;
|
|
5126
|
+
const access2 = this.deps.imageAccess === void 0 ? void 0 : (ref) => {
|
|
5127
|
+
try {
|
|
5128
|
+
return this.deps.imageAccess?.(ref);
|
|
5129
|
+
} catch {
|
|
5130
|
+
return void 0;
|
|
5131
|
+
}
|
|
5132
|
+
};
|
|
5133
|
+
const withAccess = access2 === void 0 ? {} : { access: access2 };
|
|
5134
|
+
const problem = (message) => fail2({ code: CODES.invalidRequest, message, status: null, requestId: null, retryAfterMs: null, serverType: null, serverCode: null, notice: null });
|
|
5135
|
+
if (model2?.input.includes("image") !== true) {
|
|
5136
|
+
if (sentImages(options.messages).length > 0) this.deps.events.imagesNotSent?.(sessionId2, model2?.name ?? options.model);
|
|
5137
|
+
return { sent: null, ...withAccess };
|
|
5138
|
+
}
|
|
5139
|
+
const limits = this.deps.imageLimits ?? ENGINE_IMAGE_LIMITS;
|
|
5140
|
+
const excess = sentImages(options.messages).length - limits.maxImages;
|
|
5141
|
+
if (excess > 0) {
|
|
5142
|
+
throw new LlmError(`AdaptiqAI takes up to ${limits.maxImages} images per request; the ${excess} oldest ${excess === 1 ? "one goes" : "go"} as a note naming ${excess === 1 ? "it" : "them"}.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages: excess });
|
|
5143
|
+
}
|
|
5144
|
+
const source = this.deps.attachments?.();
|
|
5145
|
+
if (source === void 0) throw problem("This DeepSeek Harness has no attachment service, so images can't be sent. Update DeepSeek Harness and try again.");
|
|
5146
|
+
try {
|
|
5147
|
+
return { sent: await prepareImages(options.messages, source, limits, signal), ...withAccess };
|
|
5148
|
+
} catch (error) {
|
|
5149
|
+
if (error instanceof ImageProblem) throw problem(error.message);
|
|
5150
|
+
if (signal.aborted) throw new LlmError("Request stopped.", CODES.aborted, { cause: error });
|
|
5151
|
+
throw problem(`An attached image could not be read: ${error instanceof Error ? error.message : String(error)}. Attach it again.`);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
4973
5154
|
async *generate(options) {
|
|
4974
5155
|
if (options.purpose === "compaction") {
|
|
4975
5156
|
const message = "AdaptiqAI keeps the whole conversation in native memory, so it never needs compacting. Nothing was removed.";
|
|
@@ -5044,7 +5225,8 @@ var AdaptiqAdapter = class extends LlmAdapter {
|
|
|
5044
5225
|
if (references.length > 0) recalled = await this.deps.recall(sessionId2, options.model, references, signal).catch(() => NOTHING_RECALLED);
|
|
5045
5226
|
}
|
|
5046
5227
|
const parent = options.sessionId === void 0 ? void 0 : this.deps.parentSession?.(String(options.sessionId));
|
|
5047
|
-
|
|
5228
|
+
const images = await this.images(options, model2, sessionId2, fail2, signal);
|
|
5229
|
+
let body = JSON.stringify(buildChatBody(options, model2, { remember, stored, recalled, images }));
|
|
5048
5230
|
this.deps.debug?.(`adaptiq: request ${sessionId2 ?? "-"}: ${options.messages.length} messages, ${body.length} bytes, ${recalled.size} read from memory, transcripts sent: ${body.includes("<referenced-sessions>") ? "yes" : "no"}`);
|
|
5049
5231
|
let response;
|
|
5050
5232
|
let refreshed = false;
|
|
@@ -5085,7 +5267,7 @@ var AdaptiqAdapter = class extends LlmAdapter {
|
|
|
5085
5267
|
await response.body?.cancel().catch(() => {
|
|
5086
5268
|
});
|
|
5087
5269
|
stored = false;
|
|
5088
|
-
body = JSON.stringify(buildChatBody(options, model2, { remember, stored, recalled }));
|
|
5270
|
+
body = JSON.stringify(buildChatBody(options, model2, { remember, stored, recalled, images }));
|
|
5089
5271
|
continue;
|
|
5090
5272
|
}
|
|
5091
5273
|
break;
|
|
@@ -6091,7 +6273,7 @@ Open **Settings \u2192 AdaptiqAI** to connect, or run \`/adaptiq signin\`.
|
|
|
6091
6273
|
var ENTRY_PROTOCOL = 1;
|
|
6092
6274
|
var RELOAD_SIGNATURE = true ? "ada3dfffa1b3d7b1" : "source";
|
|
6093
6275
|
function pluginVersion() {
|
|
6094
|
-
if (true) return "0.2.
|
|
6276
|
+
if (true) return "0.2.13";
|
|
6095
6277
|
try {
|
|
6096
6278
|
return createRequire(import.meta.url)("../package.json").version;
|
|
6097
6279
|
} catch {
|
|
@@ -6114,14 +6296,25 @@ async function harnessSessions(ctx) {
|
|
|
6114
6296
|
return new Map(records.map((record) => {
|
|
6115
6297
|
let title = null;
|
|
6116
6298
|
try {
|
|
6117
|
-
const
|
|
6118
|
-
if (typeof
|
|
6299
|
+
const label2 = typeof labels?.projectedLabels === "function" ? labels.projectedLabels(record).label : void 0;
|
|
6300
|
+
if (typeof label2 === "string" && label2.trim() !== "" && label2 !== record.header.id) title = label2.trim();
|
|
6119
6301
|
} catch {
|
|
6120
6302
|
}
|
|
6121
6303
|
const subagent = record.header.origin === "subagent" || (record.header.delegationDepth ?? 0) > 0;
|
|
6122
6304
|
return [record.header.id, { title, subagent }];
|
|
6123
6305
|
}));
|
|
6124
6306
|
}
|
|
6307
|
+
function attachmentService(ctx) {
|
|
6308
|
+
const service = ctx.get("attachments");
|
|
6309
|
+
return typeof service?.readImageRequest === "function" ? service : void 0;
|
|
6310
|
+
}
|
|
6311
|
+
function imageAccess(ctx, ref) {
|
|
6312
|
+
const host = attachmentService(ctx)?.imageHostPath?.(ref);
|
|
6313
|
+
if (host === void 0) return void 0;
|
|
6314
|
+
const fs = ctx.get("fs");
|
|
6315
|
+
const readonlyPath = fs?.processPathFromHostPath?.(host);
|
|
6316
|
+
return readonlyPath === void 0 ? void 0 : { readonlyPath };
|
|
6317
|
+
}
|
|
6125
6318
|
var VERSION = pluginVersion();
|
|
6126
6319
|
async function apply(ctx, config) {
|
|
6127
6320
|
await applyWith(ctx, config, void 0);
|
|
@@ -6146,8 +6339,8 @@ async function applyWith(ctx, raw, runtime) {
|
|
|
6146
6339
|
onChange: () => {
|
|
6147
6340
|
workspace?.authChanged();
|
|
6148
6341
|
},
|
|
6149
|
-
onSignedIn: (
|
|
6150
|
-
workspace?.signedIn(
|
|
6342
|
+
onSignedIn: (label2) => {
|
|
6343
|
+
workspace?.signedIn(label2);
|
|
6151
6344
|
}
|
|
6152
6345
|
});
|
|
6153
6346
|
const api = new AdaptiqApi(apiBase, auth, () => ({
|
|
@@ -6229,6 +6422,8 @@ async function applyWith(ctx, raw, runtime) {
|
|
|
6229
6422
|
attachPending: (sessionId2) => owned.attachPending(sessionId2),
|
|
6230
6423
|
events: owned,
|
|
6231
6424
|
autoSleep: (model2) => owned.autoSleep(model2),
|
|
6425
|
+
attachments: () => attachmentService(ctx),
|
|
6426
|
+
imageAccess: (ref) => imageAccess(ctx, ref),
|
|
6232
6427
|
...process.env.DSH_ADAPTIQ_DEBUG_LOG === void 0 || process.env.DSH_ADAPTIQ_DEBUG_LOG === "" ? {} : { debug: (message) => {
|
|
6233
6428
|
log("info", message);
|
|
6234
6429
|
} }
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, PreparedAdapterCall, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
9
9
|
import { type AdaptiqFailure, type AuthManager, type CatalogModel, type RequestEvents, type UserTurn } from '@adaptiqai/core';
|
|
10
|
+
import { type ImageLimits, type ImageRef, type ImageSource } from './images.ts';
|
|
10
11
|
import { type ParentSession, type ReferencedSession } from './wire.ts';
|
|
11
12
|
/** What the adapter tells the rest of the plugin about each request. */
|
|
12
13
|
export interface AdapterDeps {
|
|
@@ -42,6 +43,14 @@ export interface AdapterDeps {
|
|
|
42
43
|
readonly events: RequestEvents;
|
|
43
44
|
/** Whether the server behind a model puts idle conversations to sleep (it reports session history apart). */
|
|
44
45
|
readonly autoSleep?: (model: string) => boolean;
|
|
46
|
+
/** The harness's attachment service, where attached images are stored (absent on hosts without one). */
|
|
47
|
+
attachments?(): ImageSource | undefined;
|
|
48
|
+
/** Where the model's tools can read the stored copy of an image. */
|
|
49
|
+
imageAccess?(ref: ImageRef): {
|
|
50
|
+
readonlyPath: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
/** The engine's image limits (tests use smaller ones). */
|
|
53
|
+
readonly imageLimits?: ImageLimits;
|
|
45
54
|
/** Diagnostics for one request (no content), when a debug log is on. */
|
|
46
55
|
debug?(message: string): void;
|
|
47
56
|
readonly fetch?: typeof fetch;
|
|
@@ -78,5 +87,11 @@ export declare class AdaptiqAdapter extends LlmAdapter {
|
|
|
78
87
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
79
88
|
prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
|
|
80
89
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
90
|
+
/**
|
|
91
|
+
* The request's images, read from the harness's stored copies, when the
|
|
92
|
+
* model's card says it reads images. Otherwise each image goes as a short
|
|
93
|
+
* note and the person is told.
|
|
94
|
+
*/
|
|
95
|
+
private images;
|
|
81
96
|
private generate;
|
|
82
97
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images a person attached, as inline data the AdaptiqAI API accepts.
|
|
3
|
+
*
|
|
4
|
+
* The harness stores each attached image once, content-addressed (sha256), and
|
|
5
|
+
* hands the adapter a durable reference. The bytes sent are read from that
|
|
6
|
+
* stored copy, never from the file the person picked, so a history image is the
|
|
7
|
+
* same bytes on every turn (the engine dedupes by content hash) even when the
|
|
8
|
+
* original file has changed since.
|
|
9
|
+
*/
|
|
10
|
+
import type { ImageBlock, RequestMessage } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
/** One image reference as the harness stores it. */
|
|
12
|
+
export type ImageRef = ImageBlock['attachment'];
|
|
13
|
+
/** Image media types the engine accepts. */
|
|
14
|
+
export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
|
|
15
|
+
/** What the engine accepts per request. */
|
|
16
|
+
export interface ImageLimits {
|
|
17
|
+
/** Image parts in one request (history included). */
|
|
18
|
+
readonly maxImages: number;
|
|
19
|
+
/** Encoded bytes of one image. */
|
|
20
|
+
readonly maxImageBytes: number;
|
|
21
|
+
/** Width × height of one image; larger ones are scaled down before sending. */
|
|
22
|
+
readonly maxPixels: number;
|
|
23
|
+
}
|
|
24
|
+
/** The engine's limits behind api.adaptiqai.com (8 images, 20 MiB and 16.8 MP each); pixels kept a little under. */
|
|
25
|
+
export declare const ENGINE_IMAGE_LIMITS: ImageLimits;
|
|
26
|
+
/** The request version the harness's attachment service makes of one stored image. */
|
|
27
|
+
export interface RequestImageVersion {
|
|
28
|
+
readonly data: Uint8Array;
|
|
29
|
+
readonly mediaType: string;
|
|
30
|
+
readonly width: number;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
}
|
|
33
|
+
/** The part of the harness's attachment service (`ctx.attachments`) the adapter reads images through. */
|
|
34
|
+
export interface ImageSource {
|
|
35
|
+
readImageRequest(ref: ImageRef, target: {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
maxBytes: number;
|
|
39
|
+
}, signal?: AbortSignal): Promise<RequestImageVersion>;
|
|
40
|
+
}
|
|
41
|
+
/** One image ready for the wire. */
|
|
42
|
+
export interface WireImage {
|
|
43
|
+
/** `data:<mime>;base64,...` */
|
|
44
|
+
readonly url: string;
|
|
45
|
+
readonly width: number;
|
|
46
|
+
readonly height: number;
|
|
47
|
+
}
|
|
48
|
+
/** An image the engine would refuse, said in plain words. */
|
|
49
|
+
export declare class ImageProblem extends Error {
|
|
50
|
+
readonly name = "ImageProblem";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The image format, read from the bytes themselves.
|
|
54
|
+
* @param data - encoded image bytes.
|
|
55
|
+
* @returns the media type, or null when the bytes are no PNG, JPEG, WebP or GIF.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sniffImageType(data: Uint8Array): ImageMediaType | null;
|
|
58
|
+
/**
|
|
59
|
+
* The size to send one image at: its own, or scaled down (aspect kept) to fit the pixel limit.
|
|
60
|
+
* @param width - stored width.
|
|
61
|
+
* @param height - stored height.
|
|
62
|
+
* @param maxPixels - width × height limit.
|
|
63
|
+
* @returns target width and height.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fitPixels(width: number, height: number, maxPixels: number): {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The images a request sends (not offloaded by the harness), one entry per occurrence, oldest first.
|
|
71
|
+
* @param messages - the request history.
|
|
72
|
+
* @returns image blocks in request order.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sentImages(messages: readonly RequestMessage[]): ImageBlock[];
|
|
75
|
+
/**
|
|
76
|
+
* Whether any message of the request carries an image.
|
|
77
|
+
* @param messages - the request history.
|
|
78
|
+
* @returns true when one does.
|
|
79
|
+
*/
|
|
80
|
+
export declare function hasImages(messages: readonly RequestMessage[]): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Read every image the request sends from the harness's stored copies, check
|
|
83
|
+
* it against the engine's limits and encode it as a data URL.
|
|
84
|
+
* @param messages - the request history.
|
|
85
|
+
* @param source - the harness's attachment service.
|
|
86
|
+
* @param limits - the engine's limits.
|
|
87
|
+
* @param signal - cancellation.
|
|
88
|
+
* @returns attachment id → wire image.
|
|
89
|
+
* @throws ImageProblem when an image is not one the engine accepts.
|
|
90
|
+
*/
|
|
91
|
+
export declare function prepareImages(messages: readonly RequestMessage[], source: ImageSource, limits: ImageLimits, signal?: AbortSignal): Promise<Map<string, WireImage>>;
|
package/lib/types/host/wire.d.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness request → OpenAI Chat Completions body. Model-specific behaviour
|
|
3
|
-
* (thinking field and values, sampling defaults) comes only from the
|
|
4
|
-
* model's catalog entry.
|
|
5
|
-
*/
|
|
6
1
|
import type { GenerateOptions, RequestMessage } from '@deepseek-ai/dsh-llm';
|
|
7
2
|
import { type CatalogModel, type SessionRole, type UserTurn } from '@adaptiqai/core';
|
|
3
|
+
import type { ImageRef, WireImage } from './images.ts';
|
|
8
4
|
/** One Chat Completions message. */
|
|
9
5
|
export type WireMessage = {
|
|
10
6
|
role: 'system';
|
|
11
7
|
content: string;
|
|
12
8
|
} | {
|
|
13
9
|
role: 'user';
|
|
14
|
-
content: string;
|
|
10
|
+
content: string | WirePart[];
|
|
15
11
|
} | {
|
|
16
12
|
role: 'assistant';
|
|
17
13
|
content: string | null;
|
|
@@ -22,6 +18,25 @@ export type WireMessage = {
|
|
|
22
18
|
tool_call_id: string;
|
|
23
19
|
content: string;
|
|
24
20
|
};
|
|
21
|
+
/** One part of a user message that carries images, in the person's order. */
|
|
22
|
+
export type WirePart = {
|
|
23
|
+
type: 'text';
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'image_url';
|
|
27
|
+
image_url: {
|
|
28
|
+
url: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/** How the images of a request go on the wire. */
|
|
32
|
+
export interface WireImages {
|
|
33
|
+
/** Attachment id → inline image; null when the model reads text only (each image becomes a short note). */
|
|
34
|
+
readonly sent: ReadonlyMap<string, WireImage> | null;
|
|
35
|
+
/** Where the model's tools can read the harness's stored copy of an image. */
|
|
36
|
+
readonly access?: (ref: ImageRef) => {
|
|
37
|
+
readonlyPath: string;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}
|
|
25
40
|
export interface WireToolCall {
|
|
26
41
|
id: string;
|
|
27
42
|
type: 'function';
|
|
@@ -40,6 +55,8 @@ export interface WireExtras {
|
|
|
40
55
|
readonly includeUsage?: boolean;
|
|
41
56
|
/** Referenced sessions attached to this conversation and ready, by session id → name. */
|
|
42
57
|
readonly recalled?: ReadonlyMap<string, string>;
|
|
58
|
+
/** The request's images; without it every image becomes a short note. */
|
|
59
|
+
readonly images?: WireImages;
|
|
43
60
|
}
|
|
44
61
|
/** One session another message mentioned with `@`, as the harness snapshot records it. */
|
|
45
62
|
export interface ReferencedSession {
|
|
@@ -94,9 +111,10 @@ export declare function userTurns(messages: readonly RequestMessage[]): UserTurn
|
|
|
94
111
|
* Convert the harness history to Chat Completions messages.
|
|
95
112
|
* @param options - the harness request.
|
|
96
113
|
* @param recalled - referenced sessions read from memory: their snapshots become one line.
|
|
114
|
+
* @param images - the request's images; without it each image becomes a short note.
|
|
97
115
|
* @returns the wire messages in order.
|
|
98
116
|
*/
|
|
99
|
-
export declare function wireMessages(options: Pick<GenerateOptions, 'messages' | 'system'>, recalled?: ReadonlyMap<string, string
|
|
117
|
+
export declare function wireMessages(options: Pick<GenerateOptions, 'messages' | 'system'>, recalled?: ReadonlyMap<string, string>, images?: WireImages): WireMessage[];
|
|
100
118
|
/**
|
|
101
119
|
* Build the request body for one call.
|
|
102
120
|
* @param options - the harness request.
|