@pure01fx/dsh-openai-codex-auth 0.8.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 +18 -0
- package/CODEX-COMPATIBILITY.md +93 -0
- package/README.md +60 -1
- package/client.js +95 -12
- package/lib/catalog.d.ts +6 -0
- package/lib/catalog.js +24 -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 +21 -1
- package/lib/index.js +601 -149
- package/lib/native-adapter.d.ts +5 -0
- package/lib/native-adapter.js +22 -10
- package/lib/native-http.d.ts +2 -0
- package/lib/native-http.js +3 -0
- package/lib/native-websocket.js +11 -5
- package/lib/replay.d.ts +1 -0
- package/lib/replay.js +8 -2
- package/lib/response-usage.d.ts +4 -2
- package/lib/response-usage.js +26 -6
- package/lib/responses.d.ts +10 -1
- package/lib/responses.js +86 -10
- package/lib/upstream.d.ts +6 -4
- package/lib/upstream.js +6 -4
- package/lib/usage.d.ts +1 -0
- package/lib/usage.js +10 -3
- package/package.json +62 -9
|
@@ -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;
|
|
@@ -132,6 +135,8 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
132
135
|
private usageError;
|
|
133
136
|
private usageRefresh;
|
|
134
137
|
private usageGeneration;
|
|
138
|
+
private accountUsageRequestGeneration;
|
|
139
|
+
private readonly accountUsageCredentialRefreshes;
|
|
135
140
|
private directUsageSequence;
|
|
136
141
|
private directUsageAccountId;
|
|
137
142
|
private usageHasDirectDefault;
|
|
@@ -156,9 +161,9 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
156
161
|
private commitDocument;
|
|
157
162
|
private upsertCurrentCredential;
|
|
158
163
|
private commitCredential;
|
|
159
|
-
private resolveManagedCredentialLocked;
|
|
160
164
|
/** Return the current managed bearer token, refreshing and migrating it when needed. */
|
|
161
165
|
bearerToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
166
|
+
private managedCredential;
|
|
162
167
|
private externalNativeCredential;
|
|
163
168
|
private resolveNativeCredential;
|
|
164
169
|
private nativeRecoveryError;
|
|
@@ -173,8 +178,22 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
173
178
|
private cancelLogin;
|
|
174
179
|
private resetCurrentAccountState;
|
|
175
180
|
private setCurrentAccount;
|
|
181
|
+
private logoutAttempt;
|
|
176
182
|
private logout;
|
|
177
183
|
private status;
|
|
184
|
+
private accountRefreshJournalFilename;
|
|
185
|
+
private readAccountRefreshJournal;
|
|
186
|
+
private writeAccountRefreshJournal;
|
|
187
|
+
private clearAccountRefreshJournal;
|
|
188
|
+
private removeDeadFileLock;
|
|
189
|
+
private withAccountRefreshLock;
|
|
190
|
+
private withAccountRefreshLocks;
|
|
191
|
+
private reconcileAccountRefreshJournal;
|
|
192
|
+
private refreshManagedAccount;
|
|
193
|
+
private persistAccountUsageCredential;
|
|
194
|
+
private refreshAccountUsageCredential;
|
|
195
|
+
private accountUsageCredential;
|
|
196
|
+
private accountUsages;
|
|
178
197
|
private fetchUsage;
|
|
179
198
|
private write;
|
|
180
199
|
private sendJson;
|
|
@@ -182,6 +201,7 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
182
201
|
private trustedManagementRequest;
|
|
183
202
|
private requireCsrf;
|
|
184
203
|
private handleStatus;
|
|
204
|
+
private handleAccountUsage;
|
|
185
205
|
private handleDeviceStart;
|
|
186
206
|
private handleBrowserStart;
|
|
187
207
|
private handleBrowserPrepare;
|