@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.
@@ -0,0 +1,129 @@
1
+ import { nativeCodexAuthorityHash } from './catalog.js';
2
+ import { buildImageGenerationRequest, buildImageEditRequest, parseImageResponse, validateImageEditArgs } from './cloud-images.js';
3
+ import { resolveImageSources, saveImageOutputs } from './cloud-media.js';
4
+ import { inspectCloudImages, parseCloudVisionArgs } from './cloud-vision.js';
5
+ import { chooseCloudModel, visibleCloudImages } from './cloud-context.js';
6
+ const imageRefs = { type: 'array', minItems: 1, maxItems: 5, description: 'Images visible in this session, or absolute file paths read under your file policy. Preserve order.', items: {
7
+ oneOf: [
8
+ { type: 'object', properties: { attachment_id: { type: 'string' } }, required: ['attachment_id'], additionalProperties: false },
9
+ { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false },
10
+ ],
11
+ } };
12
+ const common = {
13
+ prompt: { type: 'string', description: 'Describe the image to generate or the edits to perform.', minLength: 1, maxLength: 32000 },
14
+ model: { type: 'string', description: 'Optional Codex image model; defaults to configured imageModel or gpt-image-2.' },
15
+ quality: { type: 'string', enum: ['auto', 'low', 'medium', 'high'] },
16
+ background: { type: 'string', enum: ['auto', 'transparent', 'opaque'] },
17
+ size: { type: 'string', description: 'auto or a service-supported WIDTHxHEIGHT, such as 1024x1024.' },
18
+ n: { type: 'integer', minimum: 1, maximum: 4, description: 'Optional number of images; omitted by default.' },
19
+ };
20
+ const resultSchema = { type: 'object', additionalProperties: true };
21
+ function render(_args, value) {
22
+ const row = value;
23
+ const content = [{ type: 'text', text: JSON.stringify(value) }];
24
+ if (row.image_output)
25
+ for (const image of row.images ?? [])
26
+ if (image.attachment)
27
+ content.push({ type: 'image', attachment: image.attachment });
28
+ return content;
29
+ }
30
+ function requireAgent(exec) {
31
+ if (!exec.agent)
32
+ throw new Error('Codex cloud tools require an agent session');
33
+ }
34
+ const recentImages = new WeakMap();
35
+ function mediaContext(exec) {
36
+ requireAgent(exec);
37
+ const ctx = exec.agent.ctx;
38
+ const workspace = exec.agent.session.header.cwd;
39
+ if (!workspace || !ctx.fs || !ctx.attachments)
40
+ throw new Error('Codex image tools require the caller workspace, filesystem and attachment services');
41
+ const messages = exec.agent.session.deriveMessages();
42
+ const cached = recentImages.get(exec.agent.session);
43
+ const visible = visibleCloudImages(messages);
44
+ if (cached?.historyLength === messages.length) {
45
+ for (const ref of cached.refs)
46
+ if (!visible.some(value => value.attachmentId === ref.attachmentId))
47
+ visible.push(ref);
48
+ }
49
+ else
50
+ recentImages.delete(exec.agent.session);
51
+ return { fs: ctx.fs, attachments: ctx.attachments, workspace, signal: exec.signal, visibleImages: visible };
52
+ }
53
+ async function imageOutputSupported(exec) {
54
+ requireAgent(exec);
55
+ const { provider, model } = exec.agent.options;
56
+ if (!provider || !model || !exec.agent.ctx.llm)
57
+ return false;
58
+ try {
59
+ return (await exec.agent.ctx.llm.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes('image') ?? false;
60
+ }
61
+ catch {
62
+ exec.signal.throwIfAborted();
63
+ return false;
64
+ }
65
+ }
66
+ export function registerCodexImageTools(ctx, deps) {
67
+ const config = deps.config;
68
+ if (config.enabled === false)
69
+ return;
70
+ const output = { schema: resultSchema, render };
71
+ for (const kind of ['generate', 'edit']) {
72
+ if (kind === 'generate' ? config.imageGeneration === false : config.imageEditing === false)
73
+ continue;
74
+ const tool = {
75
+ name: 'codex_image_' + kind,
76
+ description: kind === 'generate' ? 'Generate images using the current Codex account. Returns original file paths and image attachments for display or further editing.'
77
+ : 'Edit 1–5 images with the current Codex account. Use explicit images OR num_last_images_to_include. Returns originals and preview attachments. Does not support mask/seed parameters.',
78
+ parameters: { type: 'object', additionalProperties: false, properties: { ...common, ...(kind === 'edit' ? {
79
+ images: imageRefs, num_last_images_to_include: { type: 'integer', minimum: 1, maximum: 5, description: 'Use the most recent visible session images; mutually exclusive with images.' },
80
+ } : {}) }, required: ['prompt'] },
81
+ output, timeoutMs: 180_000, isConcurrencySafe: () => false,
82
+ async execute(value, exec) {
83
+ requireAgent(exec);
84
+ const media = mediaContext(exec);
85
+ const scope = exec.agent.ctx;
86
+ if (!scope.shell?.sandboxMode || !scope.sandboxPolicy)
87
+ throw new Error('Codex image output requires the policy-enforcing shell and sandboxPolicy services');
88
+ const policy = scope.sandboxPolicy.resolve({ session: exec.agent.session });
89
+ const credential = await deps.client.resolveCredential(exec.signal);
90
+ const args = kind === 'edit' ? validateImageEditArgs(value, config.imageModel) : buildImageGenerationRequest(value, config.imageModel);
91
+ const request = kind === 'edit' ? buildImageEditRequest(value, await resolveImageSources(validateImageEditArgs(value, config.imageModel), media), config.imageModel) : { ...args };
92
+ const result = await deps.client.post(kind === 'edit' ? 'images/edits' : 'images/generations', request, { credential, signal: exec.signal });
93
+ const saved = await saveImageOutputs(parseImageResponse(result.body, result.requestId), { ...media, shell: scope.shell, policy });
94
+ const image_output = await imageOutputSupported(exec);
95
+ const old = recentImages.get(exec.agent.session)?.refs ?? [];
96
+ recentImages.set(exec.agent.session, { historyLength: exec.agent.session.deriveMessages().length,
97
+ refs: [...old, ...saved.images.map(image => image.attachment)].slice(-20) });
98
+ return { ...saved, image_output };
99
+ },
100
+ };
101
+ ctx.tools.register(tool);
102
+ }
103
+ if (config.imageInspection !== false)
104
+ ctx.tools.register({
105
+ name: 'codex_image_inspect',
106
+ description: 'Ask a Codex vision model to analyze images, OCR, screenshots or charts. This makes one additional cloud inference call, without recursive tools. Use explicit model or configured visionModel for non-Codex callers. Reports effective detail and actual dimensions.',
107
+ parameters: { type: 'object', additionalProperties: false, required: ['prompt', 'images'], properties: {
108
+ prompt: { type: 'string', minLength: 1, maxLength: 32000 }, images: imageRefs,
109
+ model: { type: 'string' }, detail: { type: 'string', enum: ['auto', 'low', 'high', 'original'] }, reasoning_effort: { type: 'string' },
110
+ } },
111
+ output, timeoutMs: 120_000, isConcurrencySafe: () => true,
112
+ async execute(value, exec) {
113
+ requireAgent(exec);
114
+ const args = parseCloudVisionArgs(value);
115
+ const credential = await deps.client.resolveCredential(exec.signal);
116
+ const view = await deps.catalog.listWithAuthority?.(exec.signal) ?? { models: await deps.catalog.list(exec.signal) };
117
+ if (view.authorityHash !== undefined && view.authorityHash !== nativeCodexAuthorityHash(credential.accountId))
118
+ throw new Error('Codex account changed before image inspection; retry with the current account');
119
+ const model = chooseCloudModel({ purpose: 'vision', ...(args.model === undefined ? {} : { requested: args.model }),
120
+ ...(config.visionModel === undefined ? {} : { configured: config.visionModel }),
121
+ ...exec.agent.options.provider === undefined ? {} : { currentProvider: exec.agent.options.provider },
122
+ ...exec.agent.options.model === undefined ? {} : { currentModel: exec.agent.options.model },
123
+ }, view.models);
124
+ const images = await resolveImageSources({ images: args.images }, mediaContext(exec));
125
+ return inspectCloudImages(args, { credential, model, images, signal: exec.signal, transport: deps.transport,
126
+ ...(config.visionDetail === undefined ? {} : { defaultDetail: config.visionDetail }) });
127
+ },
128
+ });
129
+ }
@@ -0,0 +1,34 @@
1
+ import { type TokenUsage } from '@deepseek-ai/dsh-llm';
2
+ import type { NativeCodexCredential, NativeCodexModel } from './catalog.js';
3
+ import { type NativeCodexHttpOptions } from './native-http.js';
4
+ import type { CodexImageDetail } from './responses.js';
5
+ import { type ImageSource } from './cloud-images.js';
6
+ import type { ResolvedCloudImage } from './cloud-media.js';
7
+ export interface CloudVisionArgs {
8
+ prompt: string;
9
+ images: ImageSource[];
10
+ model?: string;
11
+ detail?: CodexImageDetail;
12
+ reasoning_effort?: string;
13
+ }
14
+ export declare function parseCloudVisionArgs(value: unknown): CloudVisionArgs;
15
+ export interface CloudVisionDependencies {
16
+ model: NativeCodexModel;
17
+ credential: NativeCodexCredential;
18
+ images: readonly ResolvedCloudImage[];
19
+ signal: AbortSignal;
20
+ defaultDetail?: CodexImageDetail;
21
+ transport: NativeCodexHttpOptions;
22
+ }
23
+ export declare function inspectCloudImages(args: CloudVisionArgs, deps: CloudVisionDependencies): Promise<{
24
+ images: {
25
+ requested_detail: CodexImageDetail;
26
+ effective_detail: string;
27
+ width: number;
28
+ height: number;
29
+ scaled: boolean;
30
+ }[];
31
+ usage?: TokenUsage;
32
+ text: string;
33
+ model: string;
34
+ }>;
@@ -0,0 +1,108 @@
1
+ /** One explicit, tool-free Codex image understanding request. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
4
+ import { nativeCodexWireReasoningEffort } from './native-adapter.js';
5
+ import { NativeCodexHttpTransport } from './native-http.js';
6
+ import { validateImageSelection } from './cloud-images.js';
7
+ export function parseCloudVisionArgs(value) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value))
9
+ throw new Error('Vision arguments must be an object');
10
+ const row = value;
11
+ if (Object.keys(row).some(key => !['prompt', 'images', 'model', 'detail', 'reasoning_effort'].includes(key)))
12
+ throw new Error('Unknown vision argument');
13
+ if (typeof row.prompt !== 'string' || !row.prompt.trim() || Buffer.byteLength(row.prompt) > 32_000)
14
+ throw new Error('Vision prompt must be a nonempty bounded string');
15
+ const selection = validateImageSelection(row);
16
+ if (!selection.images)
17
+ throw new Error('Vision requires explicit images');
18
+ for (const key of ['model', 'reasoning_effort']) {
19
+ if (row[key] !== undefined && (typeof row[key] !== 'string' || !row[key].trim() || Buffer.byteLength(row[key]) > 256))
20
+ throw new Error('Invalid vision ' + key);
21
+ }
22
+ if (row.detail !== undefined && !['auto', 'low', 'high', 'original'].includes(row.detail))
23
+ throw new Error('Invalid vision detail');
24
+ return { prompt: row.prompt, images: selection.images,
25
+ ...(row.model === undefined ? {} : { model: row.model }),
26
+ ...(row.detail === undefined ? {} : { detail: row.detail }),
27
+ ...(row.reasoning_effort === undefined ? {} : { reasoning_effort: row.reasoning_effort }) };
28
+ }
29
+ export async function inspectCloudImages(args, deps) {
30
+ const { model } = deps;
31
+ deps.signal.throwIfAborted();
32
+ if (!model.inputModalities.includes('image'))
33
+ throw new LlmError('Select a Codex model with image input', 'VISION_UNSUPPORTED');
34
+ if (deps.images.length !== args.images.length)
35
+ throw new Error('Resolved vision images do not match the request');
36
+ const requested = args.detail ?? deps.defaultDetail ?? 'auto';
37
+ const effective = model.useResponsesLite ? undefined
38
+ : requested === 'original' && !model.supportsImageDetailOriginal ? 'high' : requested;
39
+ const images = deps.images.map(image => {
40
+ if (!image.width || !image.height)
41
+ throw new Error('Vision requires verified encoded image dimensions');
42
+ // Ephemeral references are resolved only by this invocation's reader, never persisted.
43
+ const ref = { attachmentId: ('vision-' + randomUUID()),
44
+ mediaType: image.mediaType, bytes: image.data.byteLength, width: image.width, height: image.height };
45
+ return { image, ref };
46
+ });
47
+ const signal = AbortSignal.any([deps.signal, AbortSignal.timeout(120_000)]);
48
+ let recovered = false;
49
+ const transport = new NativeCodexHttpTransport({ ...deps.transport, maxTransientRetries: 0,
50
+ resolveCredential: async (readSignal) => {
51
+ const current = recovered ? await deps.transport.resolveCredential(readSignal) : deps.credential;
52
+ if (current.accountId !== deps.credential.accountId)
53
+ throw new LlmError('Codex account changed during image inspection', 'CODEX_ACCOUNT_CHANGED');
54
+ return current;
55
+ },
56
+ recoverCredential: async (previous, readSignal) => {
57
+ const ok = await deps.transport.recoverCredential?.(previous, readSignal) ?? false;
58
+ recovered = ok;
59
+ return ok;
60
+ },
61
+ readImage: async (ref) => {
62
+ const entry = images.find(image => image.ref.attachmentId === ref.attachmentId);
63
+ if (!entry)
64
+ throw new LlmError('Image is outside this inspection request', 'INVALID_ATTACHMENT');
65
+ return { data: entry.image.data, ...(effective === undefined ? {} : { detail: effective }) };
66
+ } });
67
+ const effort = nativeCodexWireReasoningEffort(args.reasoning_effort ?? (model.useResponsesLite ? model.defaultReasoningLevel : undefined), model);
68
+ const chunks = transport.stream({ provider: 'openai-codex', model: model.slug, signal,
69
+ system: 'Analyze the provided images and answer the user request. State uncertainty where image details are unclear.',
70
+ messages: [createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: args.prompt }, ...images.map(({ ref }) => ({ type: 'image', attachment: ref }))] })],
71
+ tools: [], ...(effort === undefined ? {} : { reasoningEffort: ReasoningEffortId(effort) }) }, { pinnedAccountId: deps.credential.accountId, ...(model.useResponsesLite ? { responsesLite: {
72
+ ...(model.defaultVerbosity === undefined ? {} : { defaultVerbosity: model.defaultVerbosity }),
73
+ ...(model.instructionsTemplate === undefined ? {} : { instructionsTemplate: model.instructionsTemplate }),
74
+ } } : {}) });
75
+ const texts = new Map();
76
+ let visibleBytes = 0;
77
+ let usage;
78
+ let finished = false;
79
+ for await (const chunk of chunks) {
80
+ if (chunk.type === 'tool-call-delta' || (chunk.type === 'block-start' && chunk.blockType === 'tool-call'))
81
+ throw new LlmError('Image inspection returned an unexpected tool call', 'UNSUPPORTED');
82
+ if (chunk.type === 'text-delta') {
83
+ visibleBytes += Buffer.byteLength(chunk.text);
84
+ if (visibleBytes > 128 * 1024)
85
+ throw new LlmError('Image inspection output exceeded limit', 'RESPONSE_TOO_LARGE');
86
+ }
87
+ if (chunk.type === 'block-end' && chunk.block.type === 'text') {
88
+ texts.set(chunk.index, chunk.block.text);
89
+ if ([...texts.values()].reduce((bytes, text) => bytes + Buffer.byteLength(text), 0) > 128 * 1024)
90
+ throw new LlmError('Image inspection output exceeded limit', 'RESPONSE_TOO_LARGE');
91
+ }
92
+ if (chunk.type === 'usage')
93
+ usage = chunk.usage;
94
+ if (chunk.type === 'finish') {
95
+ if (chunk.reason.kind === 'error')
96
+ throw new LlmError(chunk.reason.failure.message, chunk.reason.failure.code);
97
+ if (chunk.reason.kind !== 'stop')
98
+ throw new LlmError('Image inspection did not complete normally', 'INCOMPLETE_RESPONSE');
99
+ finished = true;
100
+ }
101
+ }
102
+ const text = [...texts.entries()].sort(([a], [b]) => a - b).map(([, value]) => value).join('');
103
+ if (!finished || !text.trim())
104
+ throw new LlmError('Image inspection returned no completed text', 'EMPTY_RESPONSE');
105
+ return { text, model: model.slug, ...(usage === undefined ? {} : { usage }), images: images.map(({ image }) => ({
106
+ requested_detail: requested, effective_detail: effective ?? 'omitted', width: image.width, height: image.height, scaled: image.scaled,
107
+ })) };
108
+ }
@@ -0,0 +1,12 @@
1
+ /** Native codex_web registration. No private host APIs or third-party OAuth requests. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import { type NativeCodexModelCatalog } from './catalog.js';
4
+ import type { NativeCodexCloudClient } from './cloud-http.js';
5
+ export declare function registerCodexWeb(ctx: Context, deps: {
6
+ client: NativeCodexCloudClient;
7
+ catalog: NativeCodexModelCatalog;
8
+ config: {
9
+ searchModel?: string;
10
+ searchMode?: 'live' | 'indexed' | 'cached';
11
+ };
12
+ }): void;
@@ -0,0 +1,241 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm';
2
+ import { nativeCodexAuthorityHash } from './catalog.js';
3
+ import { buildCloudSearchRequest, parseCloudSearchArgs, parseCloudSearchResponse } from './cloud-search.js';
4
+ import { chooseCloudModel, recentSearchInput } from './cloud-context.js';
5
+ const str = { type: 'string' };
6
+ const uint = { type: 'integer', description: 'Nonnegative safe integer.' };
7
+ const bool = { type: 'boolean' };
8
+ const enumeration = (...values) => ({ type: 'string', enum: values });
9
+ const array = (items) => ({ type: 'array', items });
10
+ const object = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
11
+ const query = object({ q: str, recency: uint, domains: array(str) }, ['q']);
12
+ const operations = {
13
+ search_query: array(query), image_query: array(query),
14
+ open: array(object({ ref_id: str, lineno: uint }, ['ref_id'])),
15
+ click: array(object({ ref_id: str, id: uint }, ['ref_id', 'id'])),
16
+ find: array(object({ ref_id: str, pattern: str }, ['ref_id', 'pattern'])),
17
+ screenshot: array(object({ ref_id: str, pageno: uint }, ['ref_id', 'pageno'])),
18
+ finance: array(object({ ticker: str, type: enumeration('equity', 'fund', 'crypto', 'index'), market: str }, ['ticker', 'type'])),
19
+ weather: array(object({ location: str, start: { ...str, description: 'YYYY-MM-DD' }, duration: uint }, ['location'])),
20
+ sports: array(object({ tool: enumeration('sports'), fn: enumeration('schedule', 'standings'), league: enumeration('nba', 'wnba', 'nfl', 'nhl', 'mlb', 'epl', 'ncaamb', 'ncaawb', 'ipl'), team: str, opponent: str, date_from: str, date_to: str, num_games: uint, locale: str }, ['fn', 'league'])),
21
+ time: array(object({ utc_offset: { ...str, description: 'UTC offset such as +08:00.' } }, ['utc_offset'])),
22
+ response_length: enumeration('short', 'medium', 'long'),
23
+ };
24
+ const parameters = object({
25
+ commands: { ...object(operations), description: 'One or more nonempty command arrays; at most 100 operations total. Dependent actions require separate calls. screenshot only supports zero-indexed PDF pages.' },
26
+ model: { ...str, description: 'Explicit Codex catalog model; otherwise cloudTools.searchModel, then current openai-codex model. No guessed fallback.' },
27
+ context: { ...str, description: 'Optional explicit text. Default includes last two visible human texts and at most 1000 UTF-8 bytes of visible assistant text.' },
28
+ mode: enumeration('cached', 'indexed', 'live'), search_context_size: enumeration('low', 'medium', 'high'),
29
+ user_location: object({ country: str, region: str, city: str, timezone: str }),
30
+ filters: object({ allowed_domains: array(str), blocked_domains: array(str) }),
31
+ image_settings: object({ max_results: uint, caption: bool }),
32
+ }, ['commands']);
33
+ const imageSchema = object({ attachmentId: str, mediaType: enumeration('image/png', 'image/jpeg', 'image/webp', 'image/gif'), bytes: uint, width: uint, height: uint, name: str, originalDimensions: object({ width: uint, height: uint }, ['width', 'height']) }, ['attachmentId', 'mediaType', 'bytes', 'width', 'height']);
34
+ function record(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
35
+ function publicUrl(value) {
36
+ try {
37
+ const url = new URL(value);
38
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /** Source: pinned protocol/src/models.rs ContentItem / FunctionCallOutputContentItem.
45
+ * Search DTOs themselves remain opaque. Only exact known content tags are projected;
46
+ * arbitrary image_url properties and future screenshot DTOs are never guessed. */
47
+ function inputImage(value) {
48
+ return record(value) && value.type === 'input_image' && typeof value.image_url === 'string'
49
+ && (value.detail == null || ['auto', 'low', 'high', 'original'].includes(value.detail));
50
+ }
51
+ async function admitSearchImages(body, attachments, signal) {
52
+ const images = [];
53
+ const notes = new Set();
54
+ let seen = 0;
55
+ let bytes = 0;
56
+ const visit = async (item, depth) => {
57
+ signal.throwIfAborted();
58
+ if (depth > 8)
59
+ return item;
60
+ if (inputImage(item)) {
61
+ if (publicUrl(item.image_url)) {
62
+ notes.add('Remote image unavailable: the public DSH web boundary has no binary image retrieval. Image and source links are preserved.');
63
+ return item;
64
+ }
65
+ const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]*={0,2})$/.exec(item.image_url);
66
+ if (!match)
67
+ return item;
68
+ let ref;
69
+ try {
70
+ if (!attachments)
71
+ throw new Error('Attachment service unavailable');
72
+ if (++seen > 4 || match[2].length > 12 * 1024 * 1024)
73
+ throw new Error('Image count or byte limit');
74
+ const data = Buffer.from(match[2], 'base64');
75
+ bytes += data.length;
76
+ if (!data.length || bytes > 8 * 1024 * 1024 || data.toString('base64') !== match[2])
77
+ throw new Error('Invalid or oversized inline image');
78
+ signal.throwIfAborted();
79
+ ref = await attachments.saveImage({ data, mediaType: match[1] });
80
+ signal.throwIfAborted();
81
+ images.push(ref);
82
+ }
83
+ catch {
84
+ signal.throwIfAborted();
85
+ notes.add('Inline image unavailable: attachment service missing, image invalid, or image/storage limit reached. Text and source links are preserved.');
86
+ }
87
+ // Binary transfer data must not become historical text; unknown fields remain opaque.
88
+ return { ...item, image_url: ref ? '[DSH attachment ' + ref.attachmentId + ']' : '[inline image unavailable]' };
89
+ }
90
+ // Bounded structural walk permits known content blocks nested in evolving result DTOs.
91
+ if (Array.isArray(item)) {
92
+ if (item.length > 100)
93
+ return item; // Unknown large arrays stay opaque for bounded parsing.
94
+ const result = [];
95
+ for (const value of item)
96
+ result.push(await visit(value, depth + 1));
97
+ return result;
98
+ }
99
+ if (record(item) && Array.isArray(item.content) && item.content.length <= 100) {
100
+ return { ...item, content: await visit(item.content, depth + 1) };
101
+ }
102
+ return item;
103
+ };
104
+ if (!record(body) || !Array.isArray(body.results))
105
+ return { body, images, notes };
106
+ // Leave excess entries for the response parser to mark explicitly as truncated.
107
+ const results = [];
108
+ for (let i = 0; i < body.results.length; i++)
109
+ results.push(i < 100 ? await visit(body.results[i], 0) : body.results[i]);
110
+ return { body: { ...body, results }, images, notes };
111
+ }
112
+ const markerPrefix = '[codex_web context: ';
113
+ function containsReference(text, ref) {
114
+ let start = 0;
115
+ for (;;) {
116
+ const index = text.indexOf(ref, start);
117
+ if (index < 0)
118
+ return false;
119
+ const before = text[index - 1] ?? '';
120
+ const after = text[index + ref.length] ?? '';
121
+ if (!/[A-Za-z0-9_-]/.test(before) && !/[A-Za-z0-9_-]/.test(after))
122
+ return true;
123
+ start = index + ref.length;
124
+ }
125
+ }
126
+ function checkReferences(args, messages, id, recent) {
127
+ const refs = Object.values(args.commands).flatMap(value => Array.isArray(value) ? value.flatMap(row => record(row) && typeof row.ref_id === 'string' && !publicUrl(row.ref_id) ? [row.ref_id] : []) : []);
128
+ if (!refs.length)
129
+ return;
130
+ const calls = new Map();
131
+ const outputs = [];
132
+ for (const message of messages) {
133
+ if (message.role === 'assistant')
134
+ for (const block of message.content)
135
+ if (block.type === 'tool-call')
136
+ calls.set(block.id, block.name);
137
+ if (message.source.kind !== 'tool' || !['codex_web', 'run_code'].includes(calls.get(message.source.callId) ?? ''))
138
+ continue;
139
+ for (const block of message.content)
140
+ if (block.type === 'tool-result' && block.toolCallId === message.source.callId && !block.isError) {
141
+ outputs.push(block.content.filter(part => part.type === 'text').map(part => part.text).join('\n'));
142
+ }
143
+ }
144
+ outputs.push(...recent);
145
+ for (const ref of refs) {
146
+ const previous = outputs.findLast(text => containsReference(text.slice(0, text.lastIndexOf(markerPrefix)), ref) && text.includes(markerPrefix));
147
+ // Marker is emitted last, overriding any marker-looking upstream prose. It is an
148
+ // account/session digest, not a credential or an upstream encrypted continuation.
149
+ if (!previous || previous.slice(previous.lastIndexOf(markerPrefix), previous.lastIndexOf(markerPrefix) + markerPrefix.length + 64) !== markerPrefix + id) {
150
+ throw new LlmError('Search reference has no visible result in this account/session context. Search again or open its source URL; references can expire or belong to another account.', 'CODEX_SEARCH_REFERENCE_STALE');
151
+ }
152
+ }
153
+ }
154
+ function sourceLinks(results) {
155
+ const links = new Set();
156
+ const walk = (value, depth) => {
157
+ if (depth > 8 || links.size >= 100)
158
+ return;
159
+ if (typeof value === 'string' && value.length <= 4096 && publicUrl(value))
160
+ links.add(value);
161
+ else if (Array.isArray(value))
162
+ value.slice(0, 100).forEach(item => walk(item, depth + 1));
163
+ else if (record(value))
164
+ Object.values(value).slice(0, 100).forEach(item => walk(item, depth + 1));
165
+ };
166
+ walk(results, 0);
167
+ return [...links];
168
+ }
169
+ export function registerCodexWeb(ctx, deps) {
170
+ if (!ctx.tools)
171
+ return;
172
+ // Same-program Code Mode subcalls precede durable parent output. Keep a bounded
173
+ // session-object cache of already returned visible text, partitioned by context id.
174
+ const recentOutputs = new WeakMap();
175
+ const definition = {
176
+ name: 'codex_web',
177
+ description: 'Search the web/images, open/click/find results, screenshot PDF pages, and query finance/weather/sports/time through Codex. Preserve returned source URLs and ref IDs for follow-up calls. Non-Codex sessions require explicit model or cloudTools.searchModel. Search references are scoped to the current account/session; expired references require a fresh search.',
178
+ parameters: { ...parameters },
179
+ output: {
180
+ schema: object({ output: str, results: array({}), images: array(imageSchema), model: str, truncated: bool, image_output: bool }, ['output', 'model', 'truncated', 'image_output']),
181
+ render: (_args, value) => {
182
+ const result = value;
183
+ const content = [{ type: 'text', text: result.output }];
184
+ if (result.image_output)
185
+ for (const attachment of result.images ?? [])
186
+ content.push({ type: 'image', attachment });
187
+ return content;
188
+ },
189
+ },
190
+ async execute(value, exec) {
191
+ const args = parseCloudSearchArgs(value);
192
+ exec.signal.throwIfAborted();
193
+ const agent = exec.agent;
194
+ if (!agent?.session || !agent.ctx)
195
+ throw new LlmError('codex_web requires a calling DSH agent session', 'CONTEXT_REQUIRED');
196
+ const messages = agent.session.deriveMessages();
197
+ const credential = await deps.client.resolveCredential(exec.signal);
198
+ const view = await deps.catalog.listWithAuthority?.(exec.signal);
199
+ exec.signal.throwIfAborted();
200
+ if (!view || view.authorityHash !== nativeCodexAuthorityHash(credential.accountId))
201
+ throw new LlmError('Codex model catalog authority changed; retry with the current account', 'CODEX_ACCOUNT_CHANGED');
202
+ const model = chooseCloudModel({ requested: args.model, configured: deps.config.searchModel, currentProvider: agent.options.provider, currentModel: agent.options.model, purpose: 'search' }, view.models);
203
+ const input = recentSearchInput(messages).map(item => ({ type: 'message', role: item.role, content: [{ type: item.role === 'user' ? 'input_text' : 'output_text', text: item.content }] }));
204
+ const body = buildCloudSearchRequest({ ...args, mode: args.mode ?? deps.config.searchMode ?? 'live' }, { sessionId: agent.session.id, accountId: credential.accountId, model: model.slug, input });
205
+ const id = body.id;
206
+ const cached = recentOutputs.get(agent.session);
207
+ const recent = cached?.id === id ? cached.texts : [];
208
+ checkReferences(args, messages, id, recent);
209
+ const response = await deps.client.post('alpha/search', body, { credential, signal: exec.signal });
210
+ // Validate the envelope before any attachment write; image bytes are replaced
211
+ // before the final bounded result projection so large valid images can attach.
212
+ if (!record(response.body) || typeof response.body.output !== 'string' || (response.body.results != null && !Array.isArray(response.body.results)))
213
+ throw new LlmError('Invalid Codex search response', 'CODEX_CLOUD_FAILED');
214
+ const media = await admitSearchImages(response.body, agent.ctx.attachments, exec.signal);
215
+ const parsed = parseCloudSearchResponse(media.body);
216
+ let image_output = false;
217
+ const { provider: callerProvider, model: callerModel } = agent.options;
218
+ if (media.images.length && callerProvider && callerModel && agent.ctx.llm) {
219
+ try {
220
+ image_output = (await agent.ctx.llm.resolveModelInfo(callerProvider, callerModel, exec.signal)).inputModalities?.includes('image') ?? false;
221
+ }
222
+ catch {
223
+ exec.signal.throwIfAborted();
224
+ }
225
+ }
226
+ exec.signal.throwIfAborted();
227
+ let output = parsed.output;
228
+ if (parsed.results?.length)
229
+ output += '\nStructured search results:\n' + JSON.stringify(parsed.results);
230
+ const links = sourceLinks(parsed.results ?? []);
231
+ if (links.length)
232
+ output += '\nSources and result links:\n' + links.map(url => '<' + url.replace(/[<>\s]/g, c => encodeURIComponent(c)) + '>').join('\n');
233
+ if (media.notes.size)
234
+ output += '\n' + [...media.notes].join('\n');
235
+ output += '\n' + markerPrefix + id + ']';
236
+ recentOutputs.set(agent.session, { id, texts: [...recent.slice(-7), output] });
237
+ return { ...parsed, output, model: model.slug, image_output, ...(media.images.length ? { images: media.images } : {}) };
238
+ },
239
+ };
240
+ ctx.tools.register(definition);
241
+ }
package/lib/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { type IncomingMessage } from 'node:http';
5
5
  import type { WebServer } from '@deepseek-ai/dsh-host-webserver';
6
6
  import { CODEX_PROVIDER } from './native-adapter.js';
7
7
  export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
8
+ import { type CloudToolsConfig } from './cloud-tools.js';
8
9
  export { normalizeUsage } from './usage.js';
9
10
  export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
10
11
  /** Persisted OAuth credential for one ChatGPT account. */
@@ -27,6 +28,8 @@ interface ParsedCredentialDocument {
27
28
  }
28
29
  /** Plugin configuration. */
29
30
  export interface Config {
31
+ /** Optional ordinary tools backed by the current Codex cloud account. */
32
+ cloudTools?: CloudToolsConfig;
30
33
  path?: string;
31
34
  dshHome?: string;
32
35
  nativeAdapter?: boolean;
package/lib/index.js CHANGED
@@ -14,6 +14,9 @@ export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
14
14
  import { NativeCodexCatalog } from './catalog.js';
15
15
  import { NativeCodexHttpTransport } from './native-http.js';
16
16
  import { NativeCodexWebSocketTransport } from './native-websocket.js';
17
+ import { NativeCodexCloudClient } from './cloud-http.js';
18
+ import { registerCodexImageTools } from './cloud-tools.js';
19
+ import { registerCodexWeb } from './cloud-web-tool.js';
17
20
  import { mergeDirectUsage, normalizeUsage } from './usage.js';
18
21
  export { normalizeUsage } from './usage.js';
19
22
  export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
@@ -698,6 +701,17 @@ export class OpenAICodexAuth extends Service {
698
701
  nativeAdapter: z.boolean().default(true),
699
702
  nativeCompatibilityRoute: z.boolean().default(false),
700
703
  nativeWebSocket: z.boolean().default(true),
704
+ cloudTools: z.object({
705
+ enabled: z.boolean().default(true),
706
+ search: z.boolean().default(true),
707
+ imageGeneration: z.boolean().default(true),
708
+ imageEditing: z.boolean().default(true),
709
+ imageInspection: z.boolean().default(true),
710
+ searchModel: z.string(), visionModel: z.string(),
711
+ imageModel: z.string().default('gpt-image-2'),
712
+ searchMode: z.union([z.const('cached'), z.const('indexed'), z.const('live')]).default('live'),
713
+ visionDetail: z.union([z.const('auto'), z.const('low'), z.const('high'), z.const('original')]).default('auto'),
714
+ }),
701
715
  });
702
716
  static inject = ['credentials', 'webServer', 'webRuntime'];
703
717
  filename;
@@ -728,29 +742,29 @@ export class OpenAICodexAuth extends Service {
728
742
  nativeCompatibilityRoute: config.nativeCompatibilityRoute ?? false,
729
743
  nativeWebSocket: config.nativeWebSocket ?? true,
730
744
  };
745
+ const catalog = new NativeCodexCatalog({
746
+ resolveCredential: signal => this.resolveNativeCredential(signal),
747
+ warn: message => { ctx.logger.warn(message); },
748
+ });
749
+ const transportOptions = {
750
+ resolveCredential: signal => this.resolveNativeCredential(signal),
751
+ recoverCredential: (previous, signal) => this.recoverNativeCredential(previous, signal),
752
+ readImage: async (attachment, signal) => {
753
+ const store = ctx.get('attachments');
754
+ if (store === undefined) {
755
+ throw new LlmError('native Codex image input requires the attachment service', 'UNSUPPORTED');
756
+ }
757
+ return store.readImage(attachment, signal);
758
+ },
759
+ onRateLimits: observation => {
760
+ this.acceptRateLimits(observation.accountId, observation.updates);
761
+ },
762
+ onResponseUsage: observation => {
763
+ this.acceptResponseUsage(observation);
764
+ },
765
+ warn: message => { ctx.logger.warn(message); },
766
+ };
731
767
  if (this.routeConfig.nativeAdapter) {
732
- const catalog = new NativeCodexCatalog({
733
- resolveCredential: signal => this.resolveNativeCredential(signal),
734
- warn: message => { ctx.logger.warn(message); },
735
- });
736
- const transportOptions = {
737
- resolveCredential: signal => this.resolveNativeCredential(signal),
738
- recoverCredential: (previous, signal) => this.recoverNativeCredential(previous, signal),
739
- readImage: async (attachment, signal) => {
740
- const store = ctx.get('attachments');
741
- if (store === undefined) {
742
- throw new LlmError('native Codex image input requires the attachment service', 'UNSUPPORTED');
743
- }
744
- return store.readImage(attachment, signal);
745
- },
746
- onRateLimits: observation => {
747
- this.acceptRateLimits(observation.accountId, observation.updates);
748
- },
749
- onResponseUsage: observation => {
750
- this.acceptResponseUsage(observation);
751
- },
752
- warn: message => { ctx.logger.warn(message); },
753
- };
754
768
  const transport = this.routeConfig.nativeWebSocket
755
769
  ? new NativeCodexWebSocketTransport(transportOptions)
756
770
  : new NativeCodexHttpTransport(transportOptions);
@@ -764,6 +778,15 @@ export class OpenAICodexAuth extends Service {
764
778
  ], new NativeCodexAdapter(catalog, transport));
765
779
  });
766
780
  }
781
+ if (config.cloudTools?.enabled !== false) {
782
+ const cloudClient = new NativeCodexCloudClient(transportOptions);
783
+ ctx.inject(['tools'], toolCtx => {
784
+ const cloudConfig = config.cloudTools ?? {};
785
+ if (cloudConfig.search !== false)
786
+ registerCodexWeb(toolCtx, { client: cloudClient, catalog, config: cloudConfig });
787
+ registerCodexImageTools(toolCtx, { client: cloudClient, catalog, transport: transportOptions, config: cloudConfig });
788
+ });
789
+ }
767
790
  ctx.effect(async () => {
768
791
  try {
769
792
  await this.bearerToken();
@@ -9,6 +9,8 @@ type ImageBlock = Extract<ContentBlock, {
9
9
  }>;
10
10
  export interface NativeCodexImageRead {
11
11
  data: Uint8Array;
12
+ /** Internal per-image control used by the explicit vision tool. */
13
+ detail?: import('./responses.js').CodexImageDetail;
12
14
  }
13
15
  export interface NativeCodexHttpOptions {
14
16
  resolveCredential(signal?: AbortSignal): Promise<NativeCodexCredential>;