@pure01fx/dsh-openai-codex-auth 0.9.0 → 0.10.1
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/CHANGELOG.md +15 -0
- package/CODEX-COMPATIBILITY.md +99 -0
- package/README.md +62 -1
- package/client.js +23 -2
- package/lib/catalog.d.ts +1 -0
- package/lib/catalog.js +5 -0
- package/lib/cloud-context.d.ts +20 -0
- package/lib/cloud-context.js +89 -0
- package/lib/cloud-http.d.ts +28 -0
- package/lib/cloud-http.js +170 -0
- package/lib/cloud-images.d.ts +51 -0
- package/lib/cloud-images.js +122 -0
- package/lib/cloud-media.d.ts +52 -0
- package/lib/cloud-media.js +112 -0
- package/lib/cloud-search.d.ts +31 -0
- package/lib/cloud-search.js +172 -0
- package/lib/cloud-tools.d.ts +25 -0
- package/lib/cloud-tools.js +129 -0
- package/lib/cloud-vision.d.ts +34 -0
- package/lib/cloud-vision.js +108 -0
- package/lib/cloud-web-tool.d.ts +12 -0
- package/lib/cloud-web-tool.js +241 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +45 -22
- package/lib/native-http.d.ts +2 -0
- package/lib/native-http.js +1 -0
- package/lib/native-websocket-socket.js +7 -3
- package/lib/responses.d.ts +4 -2
- package/lib/responses.js +19 -5
- package/lib/upstream.d.ts +2 -0
- package/lib/upstream.js +2 -0
- package/package.json +77 -18
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Pure image endpoint validation and bounded decoding. */
|
|
2
|
+
import type { ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
3
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
4
|
+
export type ImageSource = {
|
|
5
|
+
attachment_id: string;
|
|
6
|
+
} | {
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export interface ImageSelection {
|
|
10
|
+
images?: ImageSource[];
|
|
11
|
+
num_last_images_to_include?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ImageGenerateArgs {
|
|
14
|
+
prompt: string;
|
|
15
|
+
model: string;
|
|
16
|
+
quality: 'auto' | 'low' | 'medium' | 'high';
|
|
17
|
+
background: 'auto' | 'transparent' | 'opaque';
|
|
18
|
+
size: string;
|
|
19
|
+
n?: number;
|
|
20
|
+
}
|
|
21
|
+
export type ImageEditArgs = ImageGenerateArgs & ImageSelection;
|
|
22
|
+
export declare function validateImageSelection(value: unknown): ImageSelection;
|
|
23
|
+
export declare function validateImageGenerateArgs(value: unknown, defaultModel?: string): ImageGenerateArgs;
|
|
24
|
+
export declare function validateImageEditArgs(value: unknown, defaultModel?: string): ImageEditArgs;
|
|
25
|
+
export declare function buildImageGenerationRequest(value: unknown, defaultModel?: string): ImageGenerateArgs;
|
|
26
|
+
export declare function buildImageEditRequest(value: unknown, images: readonly {
|
|
27
|
+
image_url: string;
|
|
28
|
+
}[], defaultModel?: string): {
|
|
29
|
+
images: {
|
|
30
|
+
image_url: string;
|
|
31
|
+
}[];
|
|
32
|
+
prompt: string;
|
|
33
|
+
model: string;
|
|
34
|
+
quality: "auto" | "low" | "medium" | "high";
|
|
35
|
+
background: "auto" | "transparent" | "opaque";
|
|
36
|
+
size: string;
|
|
37
|
+
n?: number;
|
|
38
|
+
};
|
|
39
|
+
export declare function decodeImageBase64(value: unknown, maxBytes?: number): Uint8Array;
|
|
40
|
+
export declare function imageMediaType(data: Uint8Array): ImageMediaType;
|
|
41
|
+
export interface ParsedCloudImage {
|
|
42
|
+
data: Uint8Array;
|
|
43
|
+
mediaType: ImageMediaType;
|
|
44
|
+
generation_id?: string;
|
|
45
|
+
metadata: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface ParsedImageResponse {
|
|
48
|
+
images: ParsedCloudImage[];
|
|
49
|
+
metadata: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
export declare function parseImageResponse(value: unknown, requestId?: string): ParsedImageResponse;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
2
|
+
function object(value, label) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4
|
+
throw new Error(label + ' must be an object');
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function text(value, label, max = 32_000) {
|
|
8
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
9
|
+
throw new Error(label + ' must be a nonempty bounded string');
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function count(value, label, max) {
|
|
13
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max)
|
|
14
|
+
throw new Error(label + ' must be an integer from 1 to ' + max);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function choice(value, label, values) {
|
|
18
|
+
if (!values.includes(value))
|
|
19
|
+
throw new Error('Invalid ' + label);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function validateImageSelection(value) {
|
|
23
|
+
const a = object(value, 'Image selection');
|
|
24
|
+
if ((a.images !== undefined) === (a.num_last_images_to_include !== undefined))
|
|
25
|
+
throw new Error('Provide exactly one of images or num_last_images_to_include');
|
|
26
|
+
if (a.images === undefined)
|
|
27
|
+
return { num_last_images_to_include: count(a.num_last_images_to_include, 'num_last_images_to_include', 5) };
|
|
28
|
+
if (!Array.isArray(a.images) || a.images.length < 1 || a.images.length > 5)
|
|
29
|
+
throw new Error('images must contain 1 to 5 references');
|
|
30
|
+
return { images: a.images.map((source) => {
|
|
31
|
+
const s = object(source, 'Image reference');
|
|
32
|
+
if (Object.keys(s).length !== 1)
|
|
33
|
+
throw new Error('Image reference must contain only attachment_id or path');
|
|
34
|
+
if (s.attachment_id !== undefined)
|
|
35
|
+
return { attachment_id: text(s.attachment_id, 'attachment_id', 512) };
|
|
36
|
+
return { path: text(s.path, 'path', 4096) };
|
|
37
|
+
}) };
|
|
38
|
+
}
|
|
39
|
+
export function validateImageGenerateArgs(value, defaultModel = 'gpt-image-2') {
|
|
40
|
+
const a = object(value, 'Image arguments');
|
|
41
|
+
const size = text(a.size ?? 'auto', 'size', 64);
|
|
42
|
+
if (size !== 'auto' && !/^[1-9][0-9]{0,4}x[1-9][0-9]{0,4}$/.test(size))
|
|
43
|
+
throw new Error('size must be auto or WIDTHxHEIGHT; model support is determined by the service');
|
|
44
|
+
return {
|
|
45
|
+
prompt: text(a.prompt, 'prompt'), model: text(a.model ?? defaultModel, 'model', 256),
|
|
46
|
+
quality: choice(a.quality ?? 'auto', 'quality', ['auto', 'low', 'medium', 'high']),
|
|
47
|
+
background: choice(a.background ?? 'auto', 'background', ['auto', 'transparent', 'opaque']), size,
|
|
48
|
+
...(a.n === undefined ? {} : { n: count(a.n, 'n', 4) }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function validateImageEditArgs(value, defaultModel) {
|
|
52
|
+
return { ...validateImageGenerateArgs(value, defaultModel), ...validateImageSelection(value) };
|
|
53
|
+
}
|
|
54
|
+
export function buildImageGenerationRequest(value, defaultModel) {
|
|
55
|
+
return validateImageGenerateArgs(value, defaultModel);
|
|
56
|
+
}
|
|
57
|
+
export function buildImageEditRequest(value, images, defaultModel) {
|
|
58
|
+
const args = validateImageEditArgs(value, defaultModel);
|
|
59
|
+
const expected = args.images?.length ?? args.num_last_images_to_include;
|
|
60
|
+
if (images.length !== expected)
|
|
61
|
+
throw new Error('Resolved image count does not match selection');
|
|
62
|
+
for (const image of images) {
|
|
63
|
+
const match = /^data:(image[/](?:png|jpeg|webp|gif));base64,(.*)$/.exec(image.image_url);
|
|
64
|
+
if (!match)
|
|
65
|
+
throw new Error('Edit images must be inline supported image data');
|
|
66
|
+
if (imageMediaType(decodeImageBase64(match[2])) !== match[1])
|
|
67
|
+
throw new Error('Edit image media type does not match bytes');
|
|
68
|
+
}
|
|
69
|
+
return { ...validateImageGenerateArgs(value, defaultModel), images: images.map(({ image_url }) => ({ image_url })) };
|
|
70
|
+
}
|
|
71
|
+
export function decodeImageBase64(value, maxBytes = MAX_IMAGE_BYTES) {
|
|
72
|
+
const cap = Math.min(maxBytes, MAX_IMAGE_BYTES);
|
|
73
|
+
if (typeof value !== 'string' || !value.length || value.length > 4 * Math.ceil(cap / 3))
|
|
74
|
+
throw new Error('Image base64 is empty or exceeds byte limit');
|
|
75
|
+
// Check length before scanning or allocating the decoded payload.
|
|
76
|
+
if (value.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(value) || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
77
|
+
throw new Error('Invalid image base64');
|
|
78
|
+
const data = Buffer.from(value, 'base64');
|
|
79
|
+
if (!data.length || data.length > cap || data.toString('base64') !== value)
|
|
80
|
+
throw new Error('Invalid image base64 or byte limit exceeded');
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
export function imageMediaType(data) {
|
|
84
|
+
const b = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
85
|
+
if (b.length >= 8 && b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
86
|
+
return 'image/png';
|
|
87
|
+
if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255)
|
|
88
|
+
return 'image/jpeg';
|
|
89
|
+
if (b.length >= 12 && b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP')
|
|
90
|
+
return 'image/webp';
|
|
91
|
+
if (b.length >= 6 && ['GIF87a', 'GIF89a'].includes(b.toString('ascii', 0, 6)))
|
|
92
|
+
return 'image/gif';
|
|
93
|
+
throw new Error('Unsupported or invalid image bytes');
|
|
94
|
+
}
|
|
95
|
+
export function parseImageResponse(value, requestId) {
|
|
96
|
+
const response = object(value, 'Image response');
|
|
97
|
+
if (!Array.isArray(response.data) || !response.data.length || response.data.length > 4)
|
|
98
|
+
throw new Error('Image response must contain 1 to 4 data entries');
|
|
99
|
+
// JSON numbers cannot preserve the entire u64 range. Reject lossy timestamps.
|
|
100
|
+
if (typeof response.created !== 'number' || !Number.isSafeInteger(response.created) || response.created < 0)
|
|
101
|
+
throw new Error('Image response created must be a lossless nonnegative u64');
|
|
102
|
+
const metadata = { created: response.created };
|
|
103
|
+
if (response.background != null)
|
|
104
|
+
metadata.background = choice(response.background, 'response background', ['auto', 'transparent', 'opaque']);
|
|
105
|
+
if (response.quality != null)
|
|
106
|
+
metadata.quality = choice(response.quality, 'response quality', ['auto', 'low', 'medium', 'high']);
|
|
107
|
+
if (response.size != null)
|
|
108
|
+
metadata.size = text(response.size, 'response size', 64);
|
|
109
|
+
if (requestId !== undefined)
|
|
110
|
+
metadata.request_id = text(requestId, 'request_id', 512);
|
|
111
|
+
let total = 0;
|
|
112
|
+
const images = response.data.map((entry) => {
|
|
113
|
+
const item = object(entry, 'Image response data');
|
|
114
|
+
const data = decodeImageBase64(item.b64_json, MAX_IMAGE_BYTES - total);
|
|
115
|
+
total += data.length;
|
|
116
|
+
const generation_id = item.generation_id == null ? undefined : text(item.generation_id, 'generation_id', 512);
|
|
117
|
+
// Unknown fields may contain payloads or private service state; never copy them to history.
|
|
118
|
+
return { data, mediaType: imageMediaType(data), metadata: generation_id === undefined ? {} : { generation_id },
|
|
119
|
+
...(generation_id === undefined ? {} : { generation_id }) };
|
|
120
|
+
});
|
|
121
|
+
return { images, metadata };
|
|
122
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
2
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
3
|
+
import type { ShellExecutor } from '@deepseek-ai/dsh-shell';
|
|
4
|
+
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
5
|
+
import { type ImageSelection, type ParsedImageResponse } from './cloud-images.js';
|
|
6
|
+
export interface MediaSourceDependencies {
|
|
7
|
+
fs: Pick<FileSystem, 'resolve' | 'readBytes' | 'processPath'>;
|
|
8
|
+
attachments: Pick<AttachmentStore, 'readImage' | 'validateImage'>;
|
|
9
|
+
/** Only verified references visible in the caller session, in chronological order. */
|
|
10
|
+
visibleImages: readonly ImageAttachmentRef[];
|
|
11
|
+
workspace: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedCloudImage {
|
|
15
|
+
image_url: string;
|
|
16
|
+
data: Uint8Array;
|
|
17
|
+
mediaType: ImageMediaType;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
scaled: boolean;
|
|
21
|
+
attachment?: ImageAttachmentRef;
|
|
22
|
+
path?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveImageSources(value: ImageSelection, deps: MediaSourceDependencies): Promise<ResolvedCloudImage[]>;
|
|
25
|
+
export interface MediaOutputDependencies {
|
|
26
|
+
fs: Pick<FileSystem, 'resolve' | 'processPath' | 'contains'>;
|
|
27
|
+
shell: Pick<ShellExecutor, 'resolve' | 'run' | 'sandboxMode'>;
|
|
28
|
+
/** Resolve from the calling session with the public sandboxPolicy service. */
|
|
29
|
+
policy: SandboxExecutionPolicy;
|
|
30
|
+
attachments: Pick<AttachmentStore, 'validateImage' | 'saveImage'>;
|
|
31
|
+
workspace: string;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
export declare function saveImageOutputs(response: ParsedImageResponse, deps: MediaOutputDependencies): Promise<{
|
|
35
|
+
images: {
|
|
36
|
+
attachment: ImageAttachmentRef;
|
|
37
|
+
preview: {
|
|
38
|
+
normalized: boolean;
|
|
39
|
+
scaled: boolean;
|
|
40
|
+
mediaType: ImageMediaType;
|
|
41
|
+
bytes: number;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
generation_id?: string;
|
|
48
|
+
path: string;
|
|
49
|
+
mediaType: ImageMediaType;
|
|
50
|
+
bytes: number;
|
|
51
|
+
}[];
|
|
52
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Caller-scoped image sources and policy-enforced original artifacts. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { posix, win32 } from 'node:path';
|
|
4
|
+
import { imageSize } from 'image-size';
|
|
5
|
+
import { MAX_IMAGE_BYTES, imageMediaType, validateImageSelection } from './cloud-images.js';
|
|
6
|
+
// Check both process-world grammars without resolving against the plugin host OS.
|
|
7
|
+
function absoluteWorldPath(path) {
|
|
8
|
+
return !path.includes('\0') && (posix.isAbsolute(path) || (win32.isAbsolute(path) && /^[A-Za-z]:|^\\\\[^\\]+\\[^\\]+/.test(path)));
|
|
9
|
+
}
|
|
10
|
+
function encodedDimensions(data) {
|
|
11
|
+
const { width, height } = imageSize(data);
|
|
12
|
+
if (![width, height].every((n) => Number.isSafeInteger(n) && n > 0))
|
|
13
|
+
throw new Error('Invalid decoded image dimensions');
|
|
14
|
+
return { width, height };
|
|
15
|
+
}
|
|
16
|
+
export async function resolveImageSources(value, deps) {
|
|
17
|
+
const selection = validateImageSelection(value);
|
|
18
|
+
const refs = new Map(deps.visibleImages.map((ref) => [String(ref.attachmentId), ref]));
|
|
19
|
+
const recent = selection.num_last_images_to_include;
|
|
20
|
+
if (recent !== undefined && deps.visibleImages.length < recent)
|
|
21
|
+
throw new Error('Not enough visible images in this session');
|
|
22
|
+
const sources = selection.images ?? deps.visibleImages.slice(-recent).map((ref) => ({ attachment_id: String(ref.attachmentId) }));
|
|
23
|
+
// Resolve every attachment id before performing any storage reads.
|
|
24
|
+
for (const source of sources)
|
|
25
|
+
if ('attachment_id' in source && !refs.has(source.attachment_id))
|
|
26
|
+
throw new Error('Image attachment is not visible in this session: ' + source.attachment_id);
|
|
27
|
+
const result = [];
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const source of sources) {
|
|
30
|
+
deps.signal?.throwIfAborted();
|
|
31
|
+
let data;
|
|
32
|
+
let attachment;
|
|
33
|
+
let path;
|
|
34
|
+
if ('attachment_id' in source) {
|
|
35
|
+
attachment = refs.get(source.attachment_id);
|
|
36
|
+
if (!Number.isSafeInteger(attachment.bytes) || attachment.bytes <= 0 || attachment.bytes > MAX_IMAGE_BYTES - total)
|
|
37
|
+
throw new Error('Image inputs exceed byte limit');
|
|
38
|
+
const stored = await deps.attachments.readImage(attachment, deps.signal);
|
|
39
|
+
data = stored.data;
|
|
40
|
+
if (stored.ref.attachmentId !== attachment.attachmentId || stored.ref.bytes !== attachment.bytes ||
|
|
41
|
+
stored.ref.mediaType !== attachment.mediaType || stored.ref.width !== attachment.width || stored.ref.height !== attachment.height ||
|
|
42
|
+
data.length !== attachment.bytes)
|
|
43
|
+
throw new Error('Stored image does not match visible attachment metadata');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (!absoluteWorldPath(source.path))
|
|
47
|
+
throw new Error('Image path must be absolute');
|
|
48
|
+
const target = await deps.fs.resolve(source.path, { cwd: deps.workspace, ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
49
|
+
data = await deps.fs.readBytes(target, deps.signal, MAX_IMAGE_BYTES - total);
|
|
50
|
+
path = deps.fs.processPath(target);
|
|
51
|
+
}
|
|
52
|
+
total += data.length;
|
|
53
|
+
if (!data.length || total > MAX_IMAGE_BYTES)
|
|
54
|
+
throw new Error('Image inputs are empty or exceed byte limit');
|
|
55
|
+
const mediaType = imageMediaType(data);
|
|
56
|
+
await deps.attachments.validateImage({ data, mediaType });
|
|
57
|
+
const dimensions = encodedDimensions(data);
|
|
58
|
+
if (attachment && (attachment.mediaType !== mediaType || attachment.width !== dimensions.width || attachment.height !== dimensions.height))
|
|
59
|
+
throw new Error('Stored image bytes do not match attachment dimensions or media type');
|
|
60
|
+
deps.signal?.throwIfAborted();
|
|
61
|
+
result.push({ ...dimensions, data, mediaType, image_url: 'data:' + mediaType + ';base64,' + Buffer.from(data).toString('base64'),
|
|
62
|
+
scaled: !!attachment?.originalDimensions,
|
|
63
|
+
...(attachment ? { attachment } : {}),
|
|
64
|
+
...(path ? { path } : {}) });
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const extensions = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif' };
|
|
69
|
+
function quote(value) { return "'" + value.replaceAll("'", "'" + '"' + "'" + '"' + "'") + "'"; }
|
|
70
|
+
// Bytes travel on stdin, never in command arguments, stdout, or the model history.
|
|
71
|
+
const WRITE_ORIGINAL = "const fs=require('node:fs');const path=require('node:path');let s='';process.stdin.setEncoding('utf8');process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{const p=process.argv[1];fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,Buffer.from(s,'base64'),{flag:'wx',mode:0o600});});";
|
|
72
|
+
export async function saveImageOutputs(response, deps) {
|
|
73
|
+
if (!absoluteWorldPath(deps.workspace))
|
|
74
|
+
throw new Error('Caller workspace must be absolute');
|
|
75
|
+
if (!deps.policy || !deps.shell.sandboxMode)
|
|
76
|
+
throw new Error('Original image writes require a policy-enforcing DSH shell');
|
|
77
|
+
let total = 0;
|
|
78
|
+
const dimensions = [];
|
|
79
|
+
for (const image of response.images) {
|
|
80
|
+
deps.signal?.throwIfAborted();
|
|
81
|
+
total += image.data.length;
|
|
82
|
+
if (!image.data.length || total > MAX_IMAGE_BYTES)
|
|
83
|
+
throw new Error('Image outputs exceed byte limit');
|
|
84
|
+
if (imageMediaType(image.data) !== image.mediaType)
|
|
85
|
+
throw new Error('Output media type does not match bytes');
|
|
86
|
+
await deps.attachments.validateImage({ data: image.data, mediaType: image.mediaType });
|
|
87
|
+
dimensions.push(encodedDimensions(image.data));
|
|
88
|
+
}
|
|
89
|
+
const workspace = await deps.fs.resolve(deps.workspace, { ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
90
|
+
const images = [];
|
|
91
|
+
for (const image of response.images) {
|
|
92
|
+
deps.signal?.throwIfAborted();
|
|
93
|
+
const name = randomUUID() + '.' + extensions[image.mediaType];
|
|
94
|
+
const target = await deps.fs.resolve('.dsh/codex-images/' + name, { cwd: deps.workspace, ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
95
|
+
if (!deps.fs.contains(workspace, target))
|
|
96
|
+
throw new Error('Original image target escapes caller workspace');
|
|
97
|
+
const path = deps.fs.processPath(target);
|
|
98
|
+
const outcome = await deps.shell.run(deps.shell.resolve({
|
|
99
|
+
command: 'node -e ' + quote(WRITE_ORIGINAL) + ' -- ' + quote(path),
|
|
100
|
+
stdin: Buffer.from(image.data).toString('base64'), workdir: deps.fs.processPath(workspace),
|
|
101
|
+
sandboxPolicy: deps.policy, signal: deps.signal, timeoutMs: 30_000, stdoutMaxBytes: 1024,
|
|
102
|
+
}));
|
|
103
|
+
if (outcome.sandbox?.denied || outcome.sandbox?.runnerFailed || outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut)
|
|
104
|
+
throw new Error('Original image write failed under caller filesystem policy');
|
|
105
|
+
const attachment = await deps.attachments.saveImage({ data: image.data, mediaType: image.mediaType, name });
|
|
106
|
+
images.push({ ...image.metadata, path, mediaType: image.mediaType, bytes: image.data.length,
|
|
107
|
+
...(image.generation_id ? { generation_id: image.generation_id } : {}),
|
|
108
|
+
...dimensions[images.length],
|
|
109
|
+
attachment, preview: { normalized: true, scaled: !!attachment.originalDimensions, mediaType: attachment.mediaType, bytes: attachment.bytes, width: attachment.width, height: attachment.height } });
|
|
110
|
+
}
|
|
111
|
+
return { ...response.metadata, images };
|
|
112
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type Json = null | boolean | number | string | Json[] | {
|
|
2
|
+
[key: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
export interface CloudSearchArgs {
|
|
5
|
+
commands: Record<string, Json>;
|
|
6
|
+
model?: string;
|
|
7
|
+
context?: string;
|
|
8
|
+
mode?: 'cached' | 'indexed' | 'live';
|
|
9
|
+
search_context_size?: 'low' | 'medium' | 'high';
|
|
10
|
+
user_location?: Record<string, string>;
|
|
11
|
+
filters?: Record<string, string[]>;
|
|
12
|
+
image_settings?: {
|
|
13
|
+
max_results?: number;
|
|
14
|
+
caption?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function parseCloudSearchArgs(value: unknown): CloudSearchArgs;
|
|
18
|
+
/** The caller selects the model and visible context using the same request credential. */
|
|
19
|
+
export declare function buildCloudSearchRequest(args: CloudSearchArgs, context: {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
accountId: string;
|
|
22
|
+
model: string;
|
|
23
|
+
input?: unknown;
|
|
24
|
+
}): Record<string, unknown>;
|
|
25
|
+
/** Keep whole opaque result entries and identify any omitted data explicitly. */
|
|
26
|
+
export declare function parseCloudSearchResponse(body: unknown): {
|
|
27
|
+
output: string;
|
|
28
|
+
results?: unknown[];
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Wire contract: Codex b348fc26674189f758d5941cdab3f78f258b2aa7, codex-api/src/search.rs. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
const MAX_REQUEST_BYTES = 256 * 1024;
|
|
5
|
+
const MAX_OUTPUT_BYTES = 128 * 1024;
|
|
6
|
+
const MAX_RESULTS_BYTES = 256 * 1024;
|
|
7
|
+
const invalid = () => new LlmError('Invalid codex_web arguments; check command fields and limits', 'INVALID_ARGS');
|
|
8
|
+
function object(value) {
|
|
9
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
10
|
+
throw invalid();
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function text(value, empty = false) {
|
|
14
|
+
if (typeof value !== 'string' || (!empty && !value.trim()) || value.length > MAX_REQUEST_BYTES)
|
|
15
|
+
throw invalid();
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
const string = value => text(value);
|
|
19
|
+
const uint = value => {
|
|
20
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
21
|
+
throw invalid();
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
const strings = value => {
|
|
25
|
+
if (!Array.isArray(value) || value.length > 100)
|
|
26
|
+
throw invalid();
|
|
27
|
+
return value.map(item => text(item));
|
|
28
|
+
};
|
|
29
|
+
const bool = value => { if (typeof value !== 'boolean')
|
|
30
|
+
throw invalid(); return value; };
|
|
31
|
+
const choice = (...values) => value => { if (typeof value !== 'string' || !values.includes(value))
|
|
32
|
+
throw invalid(); return value; };
|
|
33
|
+
const date = value => {
|
|
34
|
+
const s = text(value);
|
|
35
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s) || !Number.isFinite(Date.parse(s)) || new Date(s).toISOString().slice(0, 10) !== s)
|
|
36
|
+
throw invalid();
|
|
37
|
+
return s;
|
|
38
|
+
};
|
|
39
|
+
function fields(value, rules, required = []) {
|
|
40
|
+
const input = object(value);
|
|
41
|
+
if (required.some(key => input[key] === undefined))
|
|
42
|
+
throw invalid();
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, item] of Object.entries(input)) {
|
|
45
|
+
if (!Object.hasOwn(rules, key))
|
|
46
|
+
throw invalid();
|
|
47
|
+
if (item !== undefined)
|
|
48
|
+
result[key] = rules[key](item);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
const query = { q: string, recency: uint, domains: strings };
|
|
53
|
+
const operations = {
|
|
54
|
+
search_query: { rules: query, required: ['q'] },
|
|
55
|
+
image_query: { rules: query, required: ['q'] },
|
|
56
|
+
open: { rules: { ref_id: string, lineno: uint }, required: ['ref_id'] },
|
|
57
|
+
click: { rules: { ref_id: string, id: uint }, required: ['ref_id', 'id'] },
|
|
58
|
+
find: { rules: { ref_id: string, pattern: string }, required: ['ref_id', 'pattern'] },
|
|
59
|
+
screenshot: { rules: { ref_id: string, pageno: uint }, required: ['ref_id', 'pageno'] },
|
|
60
|
+
finance: { rules: { ticker: string, type: choice('equity', 'fund', 'crypto', 'index'), market: value => text(value, true) }, required: ['ticker', 'type'] },
|
|
61
|
+
weather: { rules: { location: string, start: date, duration: uint }, required: ['location'] },
|
|
62
|
+
sports: { rules: { tool: choice('sports'), fn: choice('schedule', 'standings'), league: choice('nba', 'wnba', 'nfl', 'nhl', 'mlb', 'epl', 'ncaamb', 'ncaawb', 'ipl'), team: string, opponent: string, date_from: date, date_to: date, num_games: uint, locale: string }, required: ['fn', 'league'] },
|
|
63
|
+
time: { rules: { utc_offset: value => {
|
|
64
|
+
const s = text(value);
|
|
65
|
+
if (!/^[+-](?:0\d|1\d|2[0-3]):[0-5]\d$/.test(s))
|
|
66
|
+
throw invalid();
|
|
67
|
+
return s;
|
|
68
|
+
} }, required: ['utc_offset'] },
|
|
69
|
+
};
|
|
70
|
+
function boundedRequest(value) {
|
|
71
|
+
try {
|
|
72
|
+
if (Buffer.byteLength(JSON.stringify(value)) > MAX_REQUEST_BYTES)
|
|
73
|
+
throw invalid();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw invalid();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function parseCloudSearchArgs(value) {
|
|
80
|
+
const input = object(value);
|
|
81
|
+
const parsed = fields(input, {
|
|
82
|
+
commands: value => {
|
|
83
|
+
const commands = object(value);
|
|
84
|
+
const result = {};
|
|
85
|
+
let count = 0;
|
|
86
|
+
for (const [key, items] of Object.entries(commands)) {
|
|
87
|
+
if (key === 'response_length') {
|
|
88
|
+
result[key] = choice('short', 'medium', 'long')(items);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!Object.hasOwn(operations, key) || !Array.isArray(items) || items.length === 0 || items.length > 100)
|
|
92
|
+
throw invalid();
|
|
93
|
+
const operation = operations[key];
|
|
94
|
+
count += items.length;
|
|
95
|
+
result[key] = items.map(item => fields(item, operation.rules, operation.required));
|
|
96
|
+
}
|
|
97
|
+
if (count === 0 || count > 100)
|
|
98
|
+
throw invalid();
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
model: string, context: value => text(value, true), mode: choice('cached', 'indexed', 'live'),
|
|
102
|
+
search_context_size: choice('low', 'medium', 'high'),
|
|
103
|
+
user_location: value => fields(value, { country: string, region: string, city: string, timezone: string }),
|
|
104
|
+
filters: value => fields(value, { allowed_domains: strings, blocked_domains: strings }),
|
|
105
|
+
image_settings: value => fields(value, { max_results: uint, caption: bool }),
|
|
106
|
+
}, ['commands']);
|
|
107
|
+
boundedRequest(parsed);
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
/** The caller selects the model and visible context using the same request credential. */
|
|
111
|
+
export function buildCloudSearchRequest(args, context) {
|
|
112
|
+
const parsed = parseCloudSearchArgs(args);
|
|
113
|
+
const id = createHash('sha256').update(JSON.stringify(['dsh-codex-search-v1', text(context.sessionId), text(context.accountId)])).digest('hex');
|
|
114
|
+
const settings = { allowed_callers: ['direct'], external_web_access: parsed.mode === 'cached' ? false : parsed.mode === 'indexed' ? 'indexed' : true };
|
|
115
|
+
for (const key of ['search_context_size', 'filters', 'image_settings'])
|
|
116
|
+
if (parsed[key] !== undefined)
|
|
117
|
+
settings[key] = parsed[key];
|
|
118
|
+
if (parsed.user_location !== undefined)
|
|
119
|
+
settings.user_location = { type: 'approximate', ...parsed.user_location };
|
|
120
|
+
const selected = parsed.context ?? context.input;
|
|
121
|
+
const input = typeof selected === 'string' ? [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: selected }] }] : selected;
|
|
122
|
+
if (input !== undefined && typeof input !== 'string' && !Array.isArray(input))
|
|
123
|
+
throw invalid();
|
|
124
|
+
const result = { id, model: text(context.model), commands: parsed.commands, settings, ...(input === undefined ? {} : { input }) };
|
|
125
|
+
boundedRequest(result);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/** Keep whole opaque result entries and identify any omitted data explicitly. */
|
|
129
|
+
export function parseCloudSearchResponse(body) {
|
|
130
|
+
let data;
|
|
131
|
+
try {
|
|
132
|
+
data = object(body);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new LlmError('Invalid Codex search response', 'CODEX_CLOUD_FAILED');
|
|
136
|
+
}
|
|
137
|
+
if (typeof data.output !== 'string' || (data.results != null && !Array.isArray(data.results)))
|
|
138
|
+
throw new LlmError('Invalid Codex search response', 'CODEX_CLOUD_FAILED');
|
|
139
|
+
let output = data.output;
|
|
140
|
+
let truncated = false;
|
|
141
|
+
if (Buffer.byteLength(output) > MAX_OUTPUT_BYTES) {
|
|
142
|
+
// Truncate only at complete UTF-8 boundaries; the notice warns that references may be omitted.
|
|
143
|
+
output = new TextDecoder().decode(Buffer.from(output).subarray(0, MAX_OUTPUT_BYTES)).replace(/\uFFFD$/, '');
|
|
144
|
+
truncated = true;
|
|
145
|
+
}
|
|
146
|
+
let results;
|
|
147
|
+
if (Array.isArray(data.results)) {
|
|
148
|
+
results = [];
|
|
149
|
+
let bytes = 2;
|
|
150
|
+
for (const item of data.results) {
|
|
151
|
+
let serialized;
|
|
152
|
+
try {
|
|
153
|
+
serialized = JSON.stringify(item);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new LlmError('Invalid Codex search results', 'CODEX_CLOUD_FAILED');
|
|
157
|
+
}
|
|
158
|
+
if (serialized === undefined)
|
|
159
|
+
throw new LlmError('Invalid Codex search results', 'CODEX_CLOUD_FAILED');
|
|
160
|
+
const size = Buffer.byteLength(serialized) + 1;
|
|
161
|
+
if (results.length >= 100 || bytes + size > MAX_RESULTS_BYTES) {
|
|
162
|
+
truncated = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
results.push(JSON.parse(serialized));
|
|
166
|
+
bytes += size;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (truncated)
|
|
170
|
+
output += '\n[Search response truncated; some content or references were omitted. Narrow the request to retrieve them.]';
|
|
171
|
+
return { output, ...(results === undefined ? {} : { results }), truncated };
|
|
172
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Ordinary DSH image tools; cloud credentials never enter tool arguments. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import { type NativeCodexModelCatalog } from './catalog.js';
|
|
4
|
+
import { NativeCodexCloudClient } from './cloud-http.js';
|
|
5
|
+
import type { NativeCodexHttpOptions } from './native-http.js';
|
|
6
|
+
import type { CodexImageDetail } from './responses.js';
|
|
7
|
+
export interface CloudToolsConfig {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
search?: boolean;
|
|
10
|
+
imageGeneration?: boolean;
|
|
11
|
+
imageEditing?: boolean;
|
|
12
|
+
imageInspection?: boolean;
|
|
13
|
+
searchModel?: string;
|
|
14
|
+
visionModel?: string;
|
|
15
|
+
imageModel?: string;
|
|
16
|
+
searchMode?: 'cached' | 'indexed' | 'live';
|
|
17
|
+
visionDetail?: CodexImageDetail;
|
|
18
|
+
}
|
|
19
|
+
export interface CloudToolsDependencies {
|
|
20
|
+
client: NativeCodexCloudClient;
|
|
21
|
+
catalog: NativeCodexModelCatalog;
|
|
22
|
+
transport: NativeCodexHttpOptions;
|
|
23
|
+
config: CloudToolsConfig;
|
|
24
|
+
}
|
|
25
|
+
export declare function registerCodexImageTools(ctx: Context, deps: CloudToolsDependencies): void;
|