@pure01fx/dsh-openai-codex-auth 0.9.0 → 0.10.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/CHANGELOG.md +8 -0
- package/CODEX-COMPATIBILITY.md +93 -0
- package/README.md +60 -1
- 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/responses.d.ts +2 -0
- package/lib/responses.js +15 -1
- package/lib/upstream.d.ts +2 -0
- package/lib/upstream.js +2 -0
- package/package.json +57 -3
|
@@ -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();
|
package/lib/native-http.d.ts
CHANGED
|
@@ -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>;
|
package/lib/native-http.js
CHANGED
|
@@ -119,6 +119,7 @@ async function resolveImage(block, options, signal) {
|
|
|
119
119
|
type: 'image',
|
|
120
120
|
mediaType: block.attachment.mediaType,
|
|
121
121
|
dataBase64: Buffer.from(stored.data).toString('base64'),
|
|
122
|
+
...stored.detail === undefined ? {} : { detail: stored.detail },
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
async function resolveToolResult(block, options, signal) {
|
package/lib/responses.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { CallId, LlmError, type ContentBlock, type GenerateOptions, type StreamC
|
|
|
2
2
|
import { type ParseSseOptions } from './sse.js';
|
|
3
3
|
import { type NativeCodexReplaySource } from './replay.js';
|
|
4
4
|
export declare const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, an AI coding agent. Help the user with software engineering tasks.";
|
|
5
|
+
export type CodexImageDetail = 'auto' | 'low' | 'high' | 'original';
|
|
5
6
|
export interface ResolvedImagePart {
|
|
6
7
|
type: 'image';
|
|
7
8
|
mediaType: string;
|
|
8
9
|
dataBase64: string;
|
|
10
|
+
detail?: CodexImageDetail;
|
|
9
11
|
}
|
|
10
12
|
export interface ResolvedToolResultPart {
|
|
11
13
|
type: 'tool-result';
|
package/lib/responses.js
CHANGED
|
@@ -21,7 +21,11 @@ function imageItem(image) {
|
|
|
21
21
|
if (!/^image[/][a-z0-9.+-]+$/i.test(image.mediaType) || image.dataBase64.length === 0) {
|
|
22
22
|
throw fixedError('native Codex request contains invalid resolved image data', 'MALFORMED_REQUEST');
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
if (image.detail !== undefined && !['auto', 'low', 'high', 'original'].includes(image.detail)) {
|
|
25
|
+
throw fixedError('native Codex image detail is invalid', 'MALFORMED_REQUEST');
|
|
26
|
+
}
|
|
27
|
+
return { type: 'input_image', image_url: `data:${image.mediaType};base64,${image.dataBase64}`,
|
|
28
|
+
...image.detail === undefined ? {} : { detail: image.detail } };
|
|
25
29
|
}
|
|
26
30
|
function toolOutput(block) {
|
|
27
31
|
const images = block.content.some(part => part.type === 'image');
|
|
@@ -169,6 +173,16 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
169
173
|
const resolved = toResponsesInput(messages, options.system);
|
|
170
174
|
const instructions = resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS;
|
|
171
175
|
const input = normalizeCodexCallIds(resolved.input);
|
|
176
|
+
if (mode.responsesLite !== undefined) {
|
|
177
|
+
for (const item of input) {
|
|
178
|
+
const parts = item.type === 'function_call_output' ? item.output : item.content;
|
|
179
|
+
if (Array.isArray(parts)) {
|
|
180
|
+
for (const part of parts)
|
|
181
|
+
if (part.type === 'input_image')
|
|
182
|
+
delete part.detail;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
172
186
|
const common = {
|
|
173
187
|
model: options.model,
|
|
174
188
|
tool_choice: 'auto',
|
package/lib/upstream.d.ts
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export declare const TRACKED_CODEX_REPOSITORY = "https://github.com/openai/codex.git";
|
|
3
3
|
export declare const TRACKED_CODEX_COMMIT = "ddf04ad26789d040f9ef6a96736f76602e35a6cc";
|
|
4
4
|
export declare const TRACKED_CODEX_RELEASE = "main@ddf04ad";
|
|
5
|
+
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
|
+
export declare const TRACKED_CODEX_CLOUD_COMMIT = "b348fc26674189f758d5941cdab3f78f258b2aa7";
|
|
5
7
|
/** Whole stable release version sent to the Codex model-catalog endpoint. */
|
|
6
8
|
export declare const CODEX_CLIENT_VERSION = "0.153.4";
|
package/lib/upstream.js
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export const TRACKED_CODEX_REPOSITORY = 'https://github.com/openai/codex.git';
|
|
3
3
|
export const TRACKED_CODEX_COMMIT = 'ddf04ad26789d040f9ef6a96736f76602e35a6cc';
|
|
4
4
|
export const TRACKED_CODEX_RELEASE = 'main@ddf04ad';
|
|
5
|
+
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
|
+
export const TRACKED_CODEX_CLOUD_COMMIT = 'b348fc26674189f758d5941cdab3f78f258b2aa7';
|
|
5
7
|
/** Whole stable release version sent to the Codex model-catalog endpoint. */
|
|
6
8
|
export const CODEX_CLIENT_VERSION = '0.153.4';
|