codsh-bundle 0.7.0 → 0.8.0
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/index.js +102 -17
- package/lib/types/vision.d.ts +34 -5
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
6
6
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
7
7
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
8
8
|
import { admitEncodedImages, isImageAdmissionError } from "@deepseek-ai/dsh-attachment";
|
|
9
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
11
11
|
import { homedir, tmpdir } from "node:os";
|
|
12
12
|
import stringWidth from "string-width";
|
|
@@ -4970,6 +4970,8 @@ var Spinner = class {
|
|
|
4970
4970
|
//#region src/vision.ts
|
|
4971
4971
|
/** How long one description may take before the paste falls back to file-only. */
|
|
4972
4972
|
const VISION_TIMEOUT_MS = 3e4;
|
|
4973
|
+
/** The sibling route that lends sight to DeepSeek's text-only models. */
|
|
4974
|
+
const DEEPSEEK_VISION_MODEL = "deepseek-v4-flash-vision-exp";
|
|
4973
4975
|
/**
|
|
4974
4976
|
* The one instruction the sidecar gets.
|
|
4975
4977
|
*
|
|
@@ -4978,6 +4980,8 @@ const VISION_TIMEOUT_MS = 3e4;
|
|
|
4978
4980
|
* code is exactly the part the coding agent needed.
|
|
4979
4981
|
*/
|
|
4980
4982
|
const VISION_PROMPT = "You are the eyes for a text-only coding agent. Describe this image precisely and completely. Transcribe ALL visible text, code, commands, error messages, numbers and labels verbatim. When it shows a UI, terminal, diagram or chart, describe its structure and layout so the agent can reason about it. Do not speculate beyond what is visible.";
|
|
4983
|
+
/** How the conversation model should consume a successful visual handoff. */
|
|
4984
|
+
const DESCRIPTION_HANDLING = "A vision model has already inspected this image. Treat the description as the image content available to you and answer the user directly. Do not say that you cannot see, read, access, or directly inspect the image, and do not mention the description, metadata, or vision handoff unless the user asks how image handling works.";
|
|
4981
4985
|
/**
|
|
4982
4986
|
* The sidecar from the environment, or undefined when none is configured.
|
|
4983
4987
|
* @param env - the process environment.
|
|
@@ -5032,6 +5036,54 @@ async function describeImage(image, config, signal) {
|
|
|
5032
5036
|
return text;
|
|
5033
5037
|
}
|
|
5034
5038
|
/**
|
|
5039
|
+
* Ask DeepSeek's native vision sibling to describe one durable image.
|
|
5040
|
+
*
|
|
5041
|
+
* This is deliberately a one-shot auxiliary call: it receives no conversation
|
|
5042
|
+
* history, system prompt, or tools, and its answer is returned as plain text
|
|
5043
|
+
* for the selected text model's ordinary user turn.
|
|
5044
|
+
* @param image - the image reference already admitted to the durable store.
|
|
5045
|
+
* @param llm - the provider-neutral runtime serving the DeepSeek route.
|
|
5046
|
+
* @param options - optional caller cancellation and request attribution.
|
|
5047
|
+
* @returns the visible text emitted by the vision model.
|
|
5048
|
+
* @throws when the route cannot see, the provider fails, the call is aborted,
|
|
5049
|
+
* or the response contains no visible text.
|
|
5050
|
+
*/
|
|
5051
|
+
async function describeImageWithLlm(image, llm, options = {}) {
|
|
5052
|
+
const timeout = AbortSignal.timeout(VISION_TIMEOUT_MS);
|
|
5053
|
+
const signal = options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout]);
|
|
5054
|
+
const prepared = await llm.prepareCall({
|
|
5055
|
+
provider: "deepseek-official",
|
|
5056
|
+
model: DEEPSEEK_VISION_MODEL,
|
|
5057
|
+
reasoningEffort: ReasoningEffortId("off")
|
|
5058
|
+
}, signal);
|
|
5059
|
+
if (prepared.inputModalities?.includes("image") !== true) throw new Error(`${DEEPSEEK_VISION_MODEL} does not accept image input`);
|
|
5060
|
+
const assembler = new BlockAssembler();
|
|
5061
|
+
const message = createUserMessage({
|
|
5062
|
+
content: [{
|
|
5063
|
+
type: "image",
|
|
5064
|
+
attachment: image
|
|
5065
|
+
}, {
|
|
5066
|
+
type: "text",
|
|
5067
|
+
text: VISION_PROMPT
|
|
5068
|
+
}],
|
|
5069
|
+
source: {
|
|
5070
|
+
kind: "plugin",
|
|
5071
|
+
plugin: "coding-cli"
|
|
5072
|
+
}
|
|
5073
|
+
});
|
|
5074
|
+
for await (const chunk of prepared.stream({
|
|
5075
|
+
...prepared.config,
|
|
5076
|
+
messages: [message],
|
|
5077
|
+
signal,
|
|
5078
|
+
...options.sessionId === void 0 ? {} : { sessionId: options.sessionId }
|
|
5079
|
+
})) assembler.push(chunk);
|
|
5080
|
+
const finish = assembler.finish;
|
|
5081
|
+
if (finish.kind === "error" || finish.kind === "aborted") throw new Error(`vision model ${finish.kind}: ${finish.failure.message}`);
|
|
5082
|
+
const text = assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
5083
|
+
if (text === "") throw new Error("vision model answered without text");
|
|
5084
|
+
return text;
|
|
5085
|
+
}
|
|
5086
|
+
/**
|
|
5035
5087
|
* The upstream store's default admission limits, for use when no store is
|
|
5036
5088
|
* mounted: the sidecar payload is bounded by the same line either way.
|
|
5037
5089
|
*/
|
|
@@ -5087,7 +5139,7 @@ async function savePastedImage(image) {
|
|
|
5087
5139
|
*/
|
|
5088
5140
|
function pastedImageBlock(id, image, at) {
|
|
5089
5141
|
const size = at.width !== void 0 && at.height !== void 0 ? ` dimensions="${at.width}x${at.height}"` : "";
|
|
5090
|
-
const body = at.description === void 0 ? "" : `\n<description>\n${at.description}\n</description>`;
|
|
5142
|
+
const body = at.description === void 0 ? "" : `\n<description>\n${at.description}\n</description>\n<handling>\n${DESCRIPTION_HANDLING}\n</handling>`;
|
|
5091
5143
|
return `<pasted-image id="${id}" media="${image.mediaType}"${size} path="${at.path}">${body}\n</pasted-image>`;
|
|
5092
5144
|
}
|
|
5093
5145
|
/**
|
|
@@ -6163,17 +6215,19 @@ async function run(ctx, config, io) {
|
|
|
6163
6215
|
* modalities or a failed resolution get the text fallback, which degrades,
|
|
6164
6216
|
* where the block path would crash the turn.
|
|
6165
6217
|
*/
|
|
6166
|
-
const
|
|
6218
|
+
const routeImageCapability = async () => {
|
|
6167
6219
|
const current = selection.current;
|
|
6168
|
-
if (current === void 0) return
|
|
6220
|
+
if (current === void 0) return void 0;
|
|
6169
6221
|
const llm = ctx.get("llm");
|
|
6170
|
-
if (llm === void 0) return
|
|
6222
|
+
if (llm === void 0) return void 0;
|
|
6171
6223
|
try {
|
|
6172
|
-
|
|
6224
|
+
const resolved = await llm.resolveModelInfo(current.provider, current.model);
|
|
6225
|
+
return resolved.inputModalities === void 0 ? void 0 : resolved.inputModalities.includes("image");
|
|
6173
6226
|
} catch {
|
|
6174
|
-
return
|
|
6227
|
+
return;
|
|
6175
6228
|
}
|
|
6176
6229
|
};
|
|
6230
|
+
const routeAcceptsImages = async () => await routeImageCapability() === true;
|
|
6177
6231
|
/**
|
|
6178
6232
|
* Resolve a /model argument to a selection.
|
|
6179
6233
|
* @param typed - a bare model id, or an explicit `provider/model`.
|
|
@@ -6845,20 +6899,20 @@ async function run(ctx, config, io) {
|
|
|
6845
6899
|
/**
|
|
6846
6900
|
* Turn pasted images into what this turn's message can carry.
|
|
6847
6901
|
*
|
|
6848
|
-
*
|
|
6849
|
-
*
|
|
6850
|
-
*
|
|
6851
|
-
*
|
|
6852
|
-
*
|
|
6853
|
-
* turn: it flashes, and the text still goes.
|
|
6902
|
+
* An image-capable model gets first-class blocks through the durable store.
|
|
6903
|
+
* A text-only model gets each image saved as a file plus a description from
|
|
6904
|
+
* either the configured sidecar or DeepSeek's built-in Vision Exp bridge;
|
|
6905
|
+
* both ride the same message so they persist for `--resume`. A recognition
|
|
6906
|
+
* failure never loses the turn: it flashes, and the saved path still goes.
|
|
6854
6907
|
* @param images - the submission's pasted images, in token order.
|
|
6855
6908
|
* @returns blocks around the text, or undefined when there is nothing extra.
|
|
6856
6909
|
*/
|
|
6857
|
-
const prepareImages = async (images) => {
|
|
6910
|
+
const prepareImages = async (images, signal) => {
|
|
6858
6911
|
if (images.length === 0) return void 0;
|
|
6859
6912
|
const store = ctx.get("attachments");
|
|
6860
6913
|
const limits = store?.imageLimits ?? DEFAULT_IMAGE_LIMITS;
|
|
6861
|
-
|
|
6914
|
+
const currentAcceptsImages = await routeImageCapability();
|
|
6915
|
+
if (currentAcceptsImages === true && store !== void 0) try {
|
|
6862
6916
|
return {
|
|
6863
6917
|
leading: (await admitEncodedImages(store, await Promise.all(images.map((pending) => fitWithinLimits(pending.image, limits))))).map((attachment) => ({
|
|
6864
6918
|
type: "image",
|
|
@@ -6872,6 +6926,9 @@ async function run(ctx, config, io) {
|
|
|
6872
6926
|
if (!isImageAdmissionError(error)) return void 0;
|
|
6873
6927
|
}
|
|
6874
6928
|
const vision = visionConfigFromEnv(process.env);
|
|
6929
|
+
const current = selection.current;
|
|
6930
|
+
const llm = ctx.get("llm");
|
|
6931
|
+
const automaticVision = vision === void 0 && currentAcceptsImages === false && current?.provider === "deepseek-official" && llm !== void 0 && store !== void 0;
|
|
6875
6932
|
const trailing = [];
|
|
6876
6933
|
for (const pending of images) {
|
|
6877
6934
|
const at = { path: await savePastedImage(pending.image) };
|
|
@@ -6880,8 +6937,25 @@ async function run(ctx, config, io) {
|
|
|
6880
6937
|
if (vision !== void 0) {
|
|
6881
6938
|
prompt.setHint(theme.dim(` ✻ describing image #${pending.id} with ${vision.model}…`));
|
|
6882
6939
|
try {
|
|
6883
|
-
at.description = await describeImage(await fitWithinLimits(pending.image, limits), vision);
|
|
6940
|
+
at.description = await describeImage(await fitWithinLimits(pending.image, limits), vision, signal);
|
|
6884
6941
|
} catch (error) {
|
|
6942
|
+
if (signal?.aborted === true) throw error;
|
|
6943
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6944
|
+
prompt.setFlash(theme.error(truncate(` image #${pending.id}: description failed (${reason}) — attached as file only`, io.console.columns)));
|
|
6945
|
+
} finally {
|
|
6946
|
+
prompt.setHint(void 0);
|
|
6947
|
+
}
|
|
6948
|
+
} else if (automaticVision) {
|
|
6949
|
+
prompt.setHint(theme.dim(` ✻ describing image #${pending.id} with ${DEEPSEEK_VISION_MODEL}…`));
|
|
6950
|
+
try {
|
|
6951
|
+
const [attachment] = await admitEncodedImages(store, [await fitWithinLimits(pending.image, limits)]);
|
|
6952
|
+
if (attachment === void 0) throw new Error("vision image was not admitted");
|
|
6953
|
+
at.description = await describeImageWithLlm(attachment, llm, {
|
|
6954
|
+
...signal === void 0 ? {} : { signal },
|
|
6955
|
+
sessionId: live.agent.session.id
|
|
6956
|
+
});
|
|
6957
|
+
} catch (error) {
|
|
6958
|
+
if (signal?.aborted === true) throw error;
|
|
6885
6959
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6886
6960
|
prompt.setFlash(theme.error(truncate(` image #${pending.id}: description failed (${reason}) — attached as file only`, io.console.columns)));
|
|
6887
6961
|
} finally {
|
|
@@ -6899,7 +6973,18 @@ async function run(ctx, config, io) {
|
|
|
6899
6973
|
};
|
|
6900
6974
|
};
|
|
6901
6975
|
const answer = async (text, source, images = []) => {
|
|
6902
|
-
const
|
|
6976
|
+
const preparing = images.length === 0 ? void 0 : new AbortController();
|
|
6977
|
+
if (preparing !== void 0) running = preparing;
|
|
6978
|
+
let extra;
|
|
6979
|
+
try {
|
|
6980
|
+
extra = await prepareImages(images, preparing?.signal);
|
|
6981
|
+
if (preparing?.signal.aborted === true) return;
|
|
6982
|
+
} catch (error) {
|
|
6983
|
+
if (preparing?.signal.aborted === true) return;
|
|
6984
|
+
throw error;
|
|
6985
|
+
} finally {
|
|
6986
|
+
if (running === preparing) running = void 0;
|
|
6987
|
+
}
|
|
6903
6988
|
const before = totalTokens(facts(branch).usage) ?? 0;
|
|
6904
6989
|
turnBaseTokens = before;
|
|
6905
6990
|
const started = performance.now();
|
package/lib/types/vision.d.ts
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* DeepSeek Vision routes receive first-class image blocks before this module
|
|
5
5
|
* is involved. For text-only routes such as Flash and Pro, codsh gives an
|
|
6
6
|
* image two honest lives: it is always saved to a stable file the agent's
|
|
7
|
-
* tools can touch — inspect, commit, embed.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* transcribed, structure narrated. Both ride the same message the person
|
|
7
|
+
* tools can touch — inspect, commit, embed. An explicit `CODSH_VISION_*`
|
|
8
|
+
* sidecar, or DeepSeek's built-in Vision Exp bridge when no sidecar is set,
|
|
9
|
+
* can also describe it into text the model can actually read: everything in
|
|
10
|
+
* it transcribed, structure narrated. Both ride the same message the person
|
|
11
11
|
* sent, so they persist in durable history and survive `--resume`.
|
|
12
12
|
* @module codsh-bundle/src/vision
|
|
13
13
|
*/
|
|
14
|
-
import type { EncodedImageAttachment, ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment/types';
|
|
14
|
+
import type { EncodedImageAttachment, ImageAttachmentLimits, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment/types';
|
|
15
|
+
import type { GenerateOptions, LlmCallConfig, ModelModality, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
16
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
15
17
|
/** A vision sidecar: an OpenAI-compatible endpoint that can see. */
|
|
16
18
|
export interface VisionConfig {
|
|
17
19
|
/** The API base, e.g. `https://open.bigmodel.cn/api/paas/v4`. */
|
|
@@ -21,6 +23,16 @@ export interface VisionConfig {
|
|
|
21
23
|
/** The multimodal model to ask. */
|
|
22
24
|
model: string;
|
|
23
25
|
}
|
|
26
|
+
/** The sibling route that lends sight to DeepSeek's text-only models. */
|
|
27
|
+
export declare const DEEPSEEK_VISION_MODEL = "deepseek-v4-flash-vision-exp";
|
|
28
|
+
/** The provider-owned preparation seam needed for one auxiliary vision call. */
|
|
29
|
+
export interface VisionLlm {
|
|
30
|
+
prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<{
|
|
31
|
+
readonly config: LlmCallConfig;
|
|
32
|
+
readonly inputModalities?: readonly ModelModality[];
|
|
33
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
24
36
|
/**
|
|
25
37
|
* The sidecar from the environment, or undefined when none is configured.
|
|
26
38
|
* @param env - the process environment.
|
|
@@ -36,6 +48,23 @@ export declare function visionConfigFromEnv(env: Record<string, string | undefin
|
|
|
36
48
|
* @throws on timeout, a non-2xx answer, or an answer with no text.
|
|
37
49
|
*/
|
|
38
50
|
export declare function describeImage(image: EncodedImageAttachment, config: VisionConfig, signal?: AbortSignal): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Ask DeepSeek's native vision sibling to describe one durable image.
|
|
53
|
+
*
|
|
54
|
+
* This is deliberately a one-shot auxiliary call: it receives no conversation
|
|
55
|
+
* history, system prompt, or tools, and its answer is returned as plain text
|
|
56
|
+
* for the selected text model's ordinary user turn.
|
|
57
|
+
* @param image - the image reference already admitted to the durable store.
|
|
58
|
+
* @param llm - the provider-neutral runtime serving the DeepSeek route.
|
|
59
|
+
* @param options - optional caller cancellation and request attribution.
|
|
60
|
+
* @returns the visible text emitted by the vision model.
|
|
61
|
+
* @throws when the route cannot see, the provider fails, the call is aborted,
|
|
62
|
+
* or the response contains no visible text.
|
|
63
|
+
*/
|
|
64
|
+
export declare function describeImageWithLlm(image: ImageAttachmentRef, llm: VisionLlm, options?: {
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
sessionId?: SessionId;
|
|
67
|
+
}): Promise<string>;
|
|
39
68
|
/**
|
|
40
69
|
* The upstream store's default admission limits, for use when no store is
|
|
41
70
|
* mounted: the sidecar payload is bounded by the same line either way.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codsh-bundle",
|
|
3
3
|
"description": "The codsh runtime: the interactive TTY surface and code-cli agent preset, installed into a dsh profile. Users install codsh-cli (the launcher) — this package is what it registers.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "lib/index.js",
|