@dsh-enhanced/coding-subscription-provider 0.1.2 → 0.1.4
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/README.md +85 -34
- package/cordis.patch.yml +2 -0
- package/lib/adapter.d.ts +48 -3
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +375 -77
- package/lib/adapter.js.map +1 -1
- package/lib/codex-direct-auth.d.ts +34 -0
- package/lib/codex-direct-auth.d.ts.map +1 -0
- package/lib/codex-direct-auth.js +604 -0
- package/lib/codex-direct-auth.js.map +1 -0
- package/lib/codex-direct-responses.d.ts +50 -0
- package/lib/codex-direct-responses.d.ts.map +1 -0
- package/lib/codex-direct-responses.js +1585 -0
- package/lib/codex-direct-responses.js.map +1 -0
- package/lib/config.d.ts +16 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +34 -1
- package/lib/config.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +34 -4
- package/lib/index.js.map +1 -1
- package/lib/process.d.ts +6 -0
- package/lib/process.d.ts.map +1 -1
- package/lib/process.js +26 -4
- package/lib/process.js.map +1 -1
- package/lib/prompt.d.ts.map +1 -1
- package/lib/prompt.js +33 -2
- package/lib/prompt.js.map +1 -1
- package/lib/session-cwd.d.ts +26 -0
- package/lib/session-cwd.d.ts.map +1 -0
- package/lib/session-cwd.js +42 -0
- package/lib/session-cwd.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +28 -4
package/lib/adapter.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import { LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
import { verifySubscriptionAuth } from './auth.js';
|
|
4
4
|
import { discoverCodexModels, } from './codex-catalog.js';
|
|
5
|
-
import {
|
|
5
|
+
import { CodexCredentialStore } from './codex-direct-auth.js';
|
|
6
|
+
import { runCodexDirectResponses, } from './codex-direct-responses.js';
|
|
7
|
+
import { configForProvider, } from './config.js';
|
|
6
8
|
import { buildPrompt } from './prompt.js';
|
|
7
9
|
import { runCliText } from './process.js';
|
|
8
10
|
import { buildInvocation } from './providers.js';
|
|
11
|
+
import { resolveTrustedSessionCwd, TrustedSessionCwdError } from './session-cwd.js';
|
|
9
12
|
import { discoverClaudeModels, discoverCursorModels, discoverGrokModels, } from './subscription-catalog.js';
|
|
10
13
|
export const routeDefinitions = [
|
|
11
14
|
{ route: 'codex-subscription', cli: 'codex', name: 'Codex Subscription (local CLI)', maturity: 'stable' },
|
|
@@ -14,6 +17,7 @@ export const routeDefinitions = [
|
|
|
14
17
|
{ route: 'grok-subscription', cli: 'grok', name: 'Grok Subscription (local Grok Build)', maturity: 'beta' },
|
|
15
18
|
];
|
|
16
19
|
const CATALOG_TTL_MS = 5 * 60_000;
|
|
20
|
+
const CATALOG_STDERR_DIAGNOSTIC = 'model catalog probe wrote to stderr; content withheld';
|
|
17
21
|
const noAutomaticRetry = Object.freeze({
|
|
18
22
|
mode: 'normal',
|
|
19
23
|
maxRetries: 0,
|
|
@@ -34,9 +38,15 @@ function abortReason(signal) {
|
|
|
34
38
|
function cliFailure(definition, command, error) {
|
|
35
39
|
if (error instanceof Error && error.cause === 'abort')
|
|
36
40
|
return error;
|
|
41
|
+
if (error instanceof TrustedSessionCwdError) {
|
|
42
|
+
return new LlmError(`${definition.name} requires a live loop-owned session whose canonical cwd matches the configured local workspace`, 'LOCAL_SESSION_CWD_REQUIRED', { cause: error });
|
|
43
|
+
}
|
|
37
44
|
if (error instanceof Error && error.cause === 'subscription-auth') {
|
|
38
45
|
return new LlmError(`${definition.name} refused the request because a subscription-compatible login could not be verified`, 'SUBSCRIPTION_AUTH_REQUIRED', { cause: error });
|
|
39
46
|
}
|
|
47
|
+
if (error instanceof Error && error.cause === 'working-directory') {
|
|
48
|
+
return new LlmError(`${definition.name} refused the configured working directory; set config.cwd to a Git repository (relative paths resolve from the DSH process directory) and restart DSH`, 'CLI_WORKING_DIRECTORY', { cause: error });
|
|
49
|
+
}
|
|
40
50
|
if (error instanceof Error && error.cause === 'protocol') {
|
|
41
51
|
return new LlmError(`${definition.name} returned an unrecognized or incomplete event stream`, 'CLI_PROTOCOL_ERROR', { cause: error });
|
|
42
52
|
}
|
|
@@ -49,8 +59,86 @@ function cliFailure(definition, command, error) {
|
|
|
49
59
|
}
|
|
50
60
|
return new LlmError(`${definition.name} failed; verify that the official CLI is installed and logged in with the intended subscription`, 'CLI_FAILED', { cause: error });
|
|
51
61
|
}
|
|
62
|
+
export const CODEX_DIRECT_PROVIDER_HTTP_CODE = 'CODEX_DIRECT_PROVIDER_HTTP';
|
|
63
|
+
export const CODEX_DIRECT_PROVIDER_FAILURE_CODE = 'CODEX_DIRECT_PROVIDER_FAILURE';
|
|
64
|
+
export const CODEX_DIRECT_CONTENT_FILTER_CODE = 'CODEX_DIRECT_CONTENT_FILTER';
|
|
65
|
+
export const CODEX_DIRECT_TRANSPORT_ERROR_CODE = 'CODEX_DIRECT_TRANSPORT_ERROR';
|
|
66
|
+
function directFailure(definition, error) {
|
|
67
|
+
if (error instanceof LlmError)
|
|
68
|
+
return error;
|
|
69
|
+
if (error instanceof Error && error.cause === 'abort')
|
|
70
|
+
return error;
|
|
71
|
+
if (error instanceof TrustedSessionCwdError) {
|
|
72
|
+
return new LlmError(`${definition.name} requires a live loop-owned session whose canonical cwd matches the configured local workspace`, 'LOCAL_SESSION_CWD_REQUIRED', { cause: error });
|
|
73
|
+
}
|
|
74
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
75
|
+
const status = validHttpStatus(error);
|
|
76
|
+
if (cause === 'subscription-auth') {
|
|
77
|
+
return new LlmError('Codex private Responses could not use the local ChatGPT session', 'SUBSCRIPTION_AUTH_REQUIRED', status === undefined ? undefined : { status });
|
|
78
|
+
}
|
|
79
|
+
if (cause === 'timeout') {
|
|
80
|
+
return new LlmError('Codex private Responses request timed out', 'CLI_TIMEOUT');
|
|
81
|
+
}
|
|
82
|
+
if (cause === 'protocol') {
|
|
83
|
+
return new LlmError('Codex private Responses returned an unrecognized or incomplete stream', 'CLI_PROTOCOL_ERROR');
|
|
84
|
+
}
|
|
85
|
+
if (cause === 'provider-http') {
|
|
86
|
+
return new LlmError('Codex private Responses provider request failed', CODEX_DIRECT_PROVIDER_HTTP_CODE, status === undefined ? undefined : { status });
|
|
87
|
+
}
|
|
88
|
+
if (cause === 'context-window') {
|
|
89
|
+
return new LlmError('Codex private Responses exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE);
|
|
90
|
+
}
|
|
91
|
+
if (cause === 'quota') {
|
|
92
|
+
return new LlmError('Codex private Responses account quota was exhausted', QUOTA_EXCEEDED_CODE);
|
|
93
|
+
}
|
|
94
|
+
if (cause === 'provider-failure') {
|
|
95
|
+
return new LlmError('Codex private Responses reported a provider failure', CODEX_DIRECT_PROVIDER_FAILURE_CODE);
|
|
96
|
+
}
|
|
97
|
+
if (cause === 'content-filter') {
|
|
98
|
+
return new LlmError('Codex private Responses output was filtered', CODEX_DIRECT_CONTENT_FILTER_CODE);
|
|
99
|
+
}
|
|
100
|
+
if (cause === 'empty-response') {
|
|
101
|
+
return new LlmError('Codex private Responses returned no visible output', EMPTY_RESPONSE_CODE);
|
|
102
|
+
}
|
|
103
|
+
if (cause === 'transport') {
|
|
104
|
+
return new LlmError('Codex private Responses transport failed', CODEX_DIRECT_TRANSPORT_ERROR_CODE);
|
|
105
|
+
}
|
|
106
|
+
return new LlmError('Codex private Responses request failed', 'CODEX_DIRECT_RESPONSES_FAILED', { cause: error });
|
|
107
|
+
}
|
|
108
|
+
function validHttpStatus(error) {
|
|
109
|
+
if (typeof error !== 'object' || error === null)
|
|
110
|
+
return undefined;
|
|
111
|
+
const status = error.status;
|
|
112
|
+
return typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599
|
|
113
|
+
? status
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Stable code for a request that needs tool calls this text-only route cannot make. */
|
|
117
|
+
export const TOOL_CALLS_UNSUPPORTED_CODE = 'tool_calls_unsupported';
|
|
118
|
+
/**
|
|
119
|
+
* Refuse a request that supplies tool schemas.
|
|
120
|
+
*
|
|
121
|
+
* These routes drive local coding CLIs that only return assistant text; they
|
|
122
|
+
* cannot emit a `tool-call` block. Silently dropping `options.tools` would let an
|
|
123
|
+
* unattended automation appear to run while never invoking its allowlisted tools,
|
|
124
|
+
* so the request fails loudly and names the working alternative instead.
|
|
125
|
+
*/
|
|
126
|
+
function toolCallsUnsupported(definition) {
|
|
127
|
+
return new LlmError(`${definition.name} is a text-only route and cannot perform tool calls. `
|
|
128
|
+
+ 'Use a provider that emits tool calls (for example @dsh-enhanced/traex-acp-provider) '
|
|
129
|
+
+ 'for agent or automation work that requires tools.', TOOL_CALLS_UNSUPPORTED_CODE);
|
|
130
|
+
}
|
|
131
|
+
/** Report a local prompt bound through DSH's canonical overflow path so compaction can retry it. */
|
|
132
|
+
function preflightFailure(error) {
|
|
133
|
+
if (error instanceof Error && error.cause === 'prompt-limit') {
|
|
134
|
+
return new LlmError(error.message, CONTEXT_WINDOW_EXCEEDED_CODE, { cause: error });
|
|
135
|
+
}
|
|
136
|
+
return error instanceof Error ? error : new Error('coding subscription provider could not serialize the DSH request');
|
|
137
|
+
}
|
|
52
138
|
/** Classify the original transport error into a stable, credential-free outcome for diagnostics. */
|
|
53
139
|
function outcomeFor(error) {
|
|
140
|
+
if (error instanceof TrustedSessionCwdError)
|
|
141
|
+
return 'working-directory';
|
|
54
142
|
const cause = error instanceof Error ? error.cause : undefined;
|
|
55
143
|
if (cause === 'abort')
|
|
56
144
|
return 'aborted';
|
|
@@ -58,6 +146,8 @@ function outcomeFor(error) {
|
|
|
58
146
|
return 'timeout';
|
|
59
147
|
if (cause === 'subscription-auth')
|
|
60
148
|
return 'auth-required';
|
|
149
|
+
if (cause === 'working-directory')
|
|
150
|
+
return 'working-directory';
|
|
61
151
|
if (cause === 'protocol')
|
|
62
152
|
return 'protocol';
|
|
63
153
|
if (cause === 'process-exit')
|
|
@@ -89,6 +179,17 @@ function reasoningInfo(model) {
|
|
|
89
179
|
},
|
|
90
180
|
};
|
|
91
181
|
}
|
|
182
|
+
function directReasoningInfo(profile) {
|
|
183
|
+
return {
|
|
184
|
+
reasoning: {
|
|
185
|
+
efforts: profile.directReasoningEfforts.map(effort => ({
|
|
186
|
+
id: ReasoningEffortId(effort),
|
|
187
|
+
name: effort,
|
|
188
|
+
})),
|
|
189
|
+
defaultEffort: ReasoningEffortId(profile.directDefaultReasoningEffort),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
92
193
|
/** Experimental LLM-compatible facade over local, already-authenticated coding agents. */
|
|
93
194
|
export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
94
195
|
config;
|
|
@@ -98,12 +199,19 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
98
199
|
discoverClaudeModels;
|
|
99
200
|
discoverCursorModels;
|
|
100
201
|
discoverGrokModels;
|
|
202
|
+
runCodexDirect;
|
|
203
|
+
codexCredentials;
|
|
204
|
+
getAttachments;
|
|
101
205
|
verifyAuth;
|
|
102
206
|
onDiagnostic;
|
|
103
207
|
onSettled;
|
|
208
|
+
liveSessions;
|
|
209
|
+
requestAttestor;
|
|
210
|
+
now;
|
|
104
211
|
lifecycle = new AbortController();
|
|
105
212
|
codexCatalogCache;
|
|
106
213
|
subscriptionCatalogCache = new Map();
|
|
214
|
+
catalogRefreshes = new Map();
|
|
107
215
|
constructor(config, dependencies = {}) {
|
|
108
216
|
super();
|
|
109
217
|
this.config = config;
|
|
@@ -113,9 +221,23 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
113
221
|
this.discoverClaudeModels = dependencies.discoverClaudeModels ?? discoverClaudeModels;
|
|
114
222
|
this.discoverCursorModels = dependencies.discoverCursorModels ?? discoverCursorModels;
|
|
115
223
|
this.discoverGrokModels = dependencies.discoverGrokModels ?? discoverGrokModels;
|
|
224
|
+
this.runCodexDirect = dependencies.runCodexDirect ?? runCodexDirectResponses;
|
|
225
|
+
this.codexCredentials = dependencies.codexCredentials ?? new CodexCredentialStore();
|
|
226
|
+
this.getAttachments = dependencies.getAttachments ?? (() => dependencies.attachments);
|
|
116
227
|
this.verifyAuth = dependencies.verifyAuth ?? verifySubscriptionAuth;
|
|
117
228
|
this.onDiagnostic = dependencies.onDiagnostic;
|
|
118
229
|
this.onSettled = dependencies.onSettled;
|
|
230
|
+
this.liveSessions = dependencies.liveSessions;
|
|
231
|
+
this.requestAttestor = dependencies.requestAttestor;
|
|
232
|
+
this.now = dependencies.now ?? Date.now;
|
|
233
|
+
}
|
|
234
|
+
trustedCwd(request) {
|
|
235
|
+
return resolveTrustedSessionCwd({
|
|
236
|
+
request,
|
|
237
|
+
configuredCwd: this.cwd,
|
|
238
|
+
sessions: this.liveSessions,
|
|
239
|
+
...(this.requestAttestor === undefined ? {} : { attestor: this.requestAttestor }),
|
|
240
|
+
});
|
|
119
241
|
}
|
|
120
242
|
reportSettled(context) {
|
|
121
243
|
if (this.onSettled === undefined)
|
|
@@ -129,29 +251,44 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
129
251
|
}
|
|
130
252
|
providerInfo(provider) {
|
|
131
253
|
const definition = definitionFor(provider);
|
|
132
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
id: provider,
|
|
256
|
+
name: definition.cli === 'codex' && this.config.codex.transport === 'direct-responses'
|
|
257
|
+
? 'Codex Subscription (private Responses, experimental)'
|
|
258
|
+
: definition.name,
|
|
259
|
+
};
|
|
133
260
|
}
|
|
134
261
|
providerRetryPolicy(_provider) {
|
|
135
262
|
return noAutomaticRetry;
|
|
136
263
|
}
|
|
137
|
-
|
|
138
|
-
if (signal.aborted)
|
|
139
|
-
throw abortReason(signal);
|
|
264
|
+
peekCodexCatalog() {
|
|
140
265
|
const cached = this.codexCatalogCache;
|
|
141
|
-
if (cached !== undefined && cached.expiresAt >
|
|
266
|
+
if (cached !== undefined && cached.expiresAt > this.now())
|
|
267
|
+
return cached.catalog;
|
|
268
|
+
this.codexCatalogCache = undefined;
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
peekSubscriptionCatalog(provider) {
|
|
272
|
+
const cached = this.subscriptionCatalogCache.get(provider);
|
|
273
|
+
if (cached !== undefined && cached.expiresAt > this.now())
|
|
142
274
|
return cached.catalog;
|
|
275
|
+
this.subscriptionCatalogCache.delete(provider);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Observe the advisory Codex CLI catalog with bounded, prompt-free control-plane I/O.
|
|
280
|
+
* Authentication is owned by `refreshCatalog`; this method only performs discovery.
|
|
281
|
+
*/
|
|
282
|
+
async observeCodexCatalog(cwd, signal) {
|
|
283
|
+
if (signal.aborted)
|
|
284
|
+
throw abortReason(signal);
|
|
285
|
+
const cached = this.peekCodexCatalog();
|
|
286
|
+
if (cached !== undefined)
|
|
287
|
+
return cached;
|
|
143
288
|
const definition = definitionFor('codex-subscription');
|
|
144
289
|
const profile = this.config.codex;
|
|
145
290
|
try {
|
|
146
|
-
await this.
|
|
147
|
-
command: profile.command,
|
|
148
|
-
cwd: this.cwd,
|
|
149
|
-
timeoutMs: this.config.authProbeTimeoutMs,
|
|
150
|
-
maxOutputBytes: this.config.maxAuthProbeBytes,
|
|
151
|
-
extraEnvNames: this.config.extraEnvNames,
|
|
152
|
-
signal,
|
|
153
|
-
});
|
|
154
|
-
const catalog = await this.discoverCodexModels({ command: profile.command, cwd: this.cwd }, {
|
|
291
|
+
const catalog = await this.discoverCodexModels({ command: profile.command, cwd }, {
|
|
155
292
|
signal,
|
|
156
293
|
timeoutMs: this.config.authProbeTimeoutMs,
|
|
157
294
|
killGraceMs: this.config.killGraceMs,
|
|
@@ -159,23 +296,31 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
159
296
|
maxOutputBytes: this.config.maxOutputBytes,
|
|
160
297
|
maxStderrBytes: this.config.maxStderrBytes,
|
|
161
298
|
extraEnvNames: this.config.extraEnvNames,
|
|
162
|
-
onDiagnostic:
|
|
299
|
+
onDiagnostic: () => {
|
|
300
|
+
try {
|
|
301
|
+
this.onDiagnostic?.(definition.route, CATALOG_STDERR_DIAGNOSTIC);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// A diagnostic sink must never affect catalog discovery.
|
|
305
|
+
}
|
|
306
|
+
},
|
|
163
307
|
});
|
|
164
|
-
|
|
308
|
+
if (signal.aborted)
|
|
309
|
+
throw abortReason(signal);
|
|
310
|
+
this.codexCatalogCache = { catalog, expiresAt: this.now() + CATALOG_TTL_MS };
|
|
165
311
|
return catalog;
|
|
166
312
|
}
|
|
167
313
|
catch (error) {
|
|
168
314
|
this.codexCatalogCache = undefined;
|
|
169
|
-
throw
|
|
315
|
+
throw error;
|
|
170
316
|
}
|
|
171
317
|
}
|
|
172
|
-
async
|
|
318
|
+
async observeSubscriptionCatalog(provider, cwd, signal) {
|
|
173
319
|
if (signal.aborted)
|
|
174
320
|
throw abortReason(signal);
|
|
175
|
-
const cached = this.
|
|
176
|
-
if (cached !== undefined
|
|
177
|
-
return cached
|
|
178
|
-
const definition = routeDefinitions.find(candidate => candidate.cli === provider);
|
|
321
|
+
const cached = this.peekSubscriptionCatalog(provider);
|
|
322
|
+
if (cached !== undefined)
|
|
323
|
+
return cached;
|
|
179
324
|
const profile = configForProvider(this.config, provider);
|
|
180
325
|
const discoverer = provider === 'claude'
|
|
181
326
|
? this.discoverClaudeModels
|
|
@@ -183,51 +328,117 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
183
328
|
? this.discoverCursorModels
|
|
184
329
|
: this.discoverGrokModels;
|
|
185
330
|
try {
|
|
331
|
+
const catalog = await discoverer({ command: profile.command, cwd }, {
|
|
332
|
+
signal,
|
|
333
|
+
timeoutMs: this.config.authProbeTimeoutMs,
|
|
334
|
+
maxOutputBytes: this.config.maxAuthProbeBytes,
|
|
335
|
+
extraEnvNames: this.config.extraEnvNames,
|
|
336
|
+
});
|
|
337
|
+
if (signal.aborted)
|
|
338
|
+
throw abortReason(signal);
|
|
339
|
+
this.subscriptionCatalogCache.set(provider, { catalog, expiresAt: this.now() + CATALOG_TTL_MS });
|
|
340
|
+
return catalog;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.subscriptionCatalogCache.delete(provider);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
observedCatalog(provider) {
|
|
348
|
+
return provider === 'codex'
|
|
349
|
+
? this.peekCodexCatalog()
|
|
350
|
+
: this.peekSubscriptionCatalog(provider);
|
|
351
|
+
}
|
|
352
|
+
observeCatalog(provider, cwd, signal) {
|
|
353
|
+
return provider === 'codex'
|
|
354
|
+
? this.observeCodexCatalog(cwd, signal)
|
|
355
|
+
: this.observeSubscriptionCatalog(provider, cwd, signal);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Refresh one CLI catalog after the existing subscription-auth gate. Concurrent
|
|
359
|
+
* list callers share the complete auth + discovery operation, so a cold picker
|
|
360
|
+
* cannot fan out local probes. Generation never enters or waits on this path.
|
|
361
|
+
*/
|
|
362
|
+
refreshCatalog(provider, cwd) {
|
|
363
|
+
const refreshSignal = this.lifecycle.signal;
|
|
364
|
+
if (refreshSignal.aborted)
|
|
365
|
+
return Promise.reject(abortReason(refreshSignal));
|
|
366
|
+
const cached = this.observedCatalog(provider);
|
|
367
|
+
if (cached !== undefined)
|
|
368
|
+
return Promise.resolve(cached);
|
|
369
|
+
const active = this.catalogRefreshes.get(provider);
|
|
370
|
+
if (active !== undefined)
|
|
371
|
+
return active;
|
|
372
|
+
const profile = configForProvider(this.config, provider);
|
|
373
|
+
let refresh;
|
|
374
|
+
refresh = (async () => {
|
|
186
375
|
await this.verifyAuth(provider, {
|
|
187
376
|
command: profile.command,
|
|
188
|
-
cwd
|
|
377
|
+
cwd,
|
|
189
378
|
timeoutMs: this.config.authProbeTimeoutMs,
|
|
190
379
|
maxOutputBytes: this.config.maxAuthProbeBytes,
|
|
191
380
|
extraEnvNames: this.config.extraEnvNames,
|
|
192
|
-
signal,
|
|
381
|
+
signal: refreshSignal,
|
|
193
382
|
...(provider === 'grok'
|
|
194
383
|
? { userVerifiedSubscription: this.config.grok.userVerifiedSubscription }
|
|
195
384
|
: {}),
|
|
196
385
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
386
|
+
if (refreshSignal.aborted)
|
|
387
|
+
throw abortReason(refreshSignal);
|
|
388
|
+
return this.observeCatalog(provider, cwd, refreshSignal);
|
|
389
|
+
})().finally(() => {
|
|
390
|
+
if (this.catalogRefreshes.get(provider) === refresh)
|
|
391
|
+
this.catalogRefreshes.delete(provider);
|
|
392
|
+
});
|
|
393
|
+
this.catalogRefreshes.set(provider, refresh);
|
|
394
|
+
return refresh;
|
|
395
|
+
}
|
|
396
|
+
reportCatalogRefreshFailure(definition, error) {
|
|
397
|
+
if (this.lifecycle.signal.aborted)
|
|
398
|
+
return;
|
|
399
|
+
const diagnostic = `model catalog refresh failed; outcome=${outcomeFor(error)}; using configured models`;
|
|
400
|
+
try {
|
|
401
|
+
this.onDiagnostic?.(definition.route, diagnostic);
|
|
205
402
|
}
|
|
206
|
-
catch
|
|
207
|
-
|
|
208
|
-
throw cliFailure(definition, profile.command, error);
|
|
403
|
+
catch {
|
|
404
|
+
// A diagnostic sink must never turn advisory catalog failure into listModels failure.
|
|
209
405
|
}
|
|
210
406
|
}
|
|
211
407
|
async listModels(provider) {
|
|
212
408
|
const definition = definitionFor(provider);
|
|
213
409
|
const profile = configForProvider(this.config, definition.cli);
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
410
|
+
const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
|
|
411
|
+
let catalog;
|
|
412
|
+
if (!directCodex && !this.lifecycle.signal.aborted) {
|
|
413
|
+
try {
|
|
414
|
+
catalog = await this.refreshCatalog(definition.cli, this.cwd);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
this.reportCatalogRefreshFailure(definition, error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const attachments = directCodex ? this.getAttachments() : undefined;
|
|
421
|
+
const inputModalities = directCodex && attachments !== undefined
|
|
422
|
+
? ['text', 'image']
|
|
423
|
+
: ['text'];
|
|
424
|
+
const defaultModel = catalog?.models.find(model => model.id === catalog.defaultModel);
|
|
218
425
|
const entries = new Map();
|
|
219
426
|
for (const model of profile.models) {
|
|
220
427
|
entries.set(model, {
|
|
221
428
|
provider,
|
|
222
429
|
id: model,
|
|
223
|
-
name: model === 'default'
|
|
224
|
-
?
|
|
225
|
-
: model
|
|
226
|
-
|
|
227
|
-
|
|
430
|
+
name: directCodex && model === 'default'
|
|
431
|
+
? `Codex Subscription (private Responses) default (${this.config.codex.directModel})`
|
|
432
|
+
: model === 'default'
|
|
433
|
+
? `${definition.name} default${defaultModel === undefined ? '' : ` (${defaultModel.name})`}`
|
|
434
|
+
: model,
|
|
435
|
+
description: directCodex
|
|
436
|
+
? 'experimental direct private Codex Responses transport using the local ChatGPT session'
|
|
437
|
+
: `${definition.maturity} local coding-agent delegation; authentication and billing remain in the official CLI`,
|
|
438
|
+
inputModalities,
|
|
228
439
|
});
|
|
229
440
|
}
|
|
230
|
-
for (const model of catalog
|
|
441
|
+
for (const model of catalog?.models ?? []) {
|
|
231
442
|
entries.set(model.id, {
|
|
232
443
|
provider,
|
|
233
444
|
id: model.id,
|
|
@@ -245,21 +456,25 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
245
456
|
if (effectiveSignal.aborted)
|
|
246
457
|
throw abortReason(effectiveSignal);
|
|
247
458
|
const definition = definitionFor(provider);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
: await this.loadSubscriptionCatalog(definition.cli, effectiveSignal);
|
|
459
|
+
const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
|
|
460
|
+
const catalog = directCodex ? undefined : this.observedCatalog(definition.cli);
|
|
251
461
|
const catalogModel = model === 'default'
|
|
252
|
-
? catalog
|
|
253
|
-
: catalog
|
|
462
|
+
? catalog?.models.find(candidate => candidate.id === catalog.defaultModel)
|
|
463
|
+
: catalog?.models.find(candidate => candidate.id === model);
|
|
464
|
+
const attachments = directCodex ? this.getAttachments() : undefined;
|
|
254
465
|
return {
|
|
255
466
|
provider,
|
|
256
467
|
id: model,
|
|
257
|
-
name: model === 'default'
|
|
258
|
-
?
|
|
259
|
-
:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
468
|
+
name: directCodex && model === 'default'
|
|
469
|
+
? `Codex Subscription (private Responses) default (${this.config.codex.directModel})`
|
|
470
|
+
: model === 'default'
|
|
471
|
+
? `${definition.name} default${catalogModel === undefined ? '' : ` (${catalogModel.name})`}`
|
|
472
|
+
: (catalogModel?.name ?? model),
|
|
473
|
+
description: catalogModel?.description ?? (directCodex
|
|
474
|
+
? 'experimental direct private Codex Responses transport using the local ChatGPT session'
|
|
475
|
+
: `${definition.maturity} local coding-agent delegation`),
|
|
476
|
+
inputModalities: directCodex && attachments !== undefined ? ['text', 'image'] : ['text'],
|
|
477
|
+
...(directCodex ? directReasoningInfo(this.config.codex) : reasoningInfo(catalogModel)),
|
|
263
478
|
};
|
|
264
479
|
}
|
|
265
480
|
async *stream(options) {
|
|
@@ -272,7 +487,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
272
487
|
let transportContext;
|
|
273
488
|
// Track the best-known lifecycle facts so a single settled report fires on ANY exit,
|
|
274
489
|
// including a consumer that returns early after block-start or a text delta.
|
|
275
|
-
let phase = '
|
|
490
|
+
let phase = 'preflight';
|
|
276
491
|
let promptSubmissionState = 'not-submitted';
|
|
277
492
|
let outcome = 'aborted';
|
|
278
493
|
let reported = false;
|
|
@@ -295,10 +510,99 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
295
510
|
});
|
|
296
511
|
};
|
|
297
512
|
try {
|
|
513
|
+
if (signal.aborted)
|
|
514
|
+
throw abortReason(signal);
|
|
515
|
+
const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
|
|
516
|
+
// Refuse tool work before any subprocess starts: this route cannot emit a
|
|
517
|
+
// tool call, and a silent drop would look like a successful empty run.
|
|
518
|
+
if (!directCodex && options.tools !== undefined && options.tools.length > 0) {
|
|
519
|
+
outcome = 'preflight';
|
|
520
|
+
throw toolCallsUnsupported(definition);
|
|
521
|
+
}
|
|
522
|
+
let cwd;
|
|
523
|
+
try {
|
|
524
|
+
cwd = this.trustedCwd(options);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
outcome = outcomeFor(error);
|
|
528
|
+
throw cliFailure(definition, profile.command, error);
|
|
529
|
+
}
|
|
530
|
+
if (directCodex) {
|
|
531
|
+
const timeoutSignal = AbortSignal.timeout(this.config.timeoutMs);
|
|
532
|
+
const directSignal = AbortSignal.any([signal, timeoutSignal]);
|
|
533
|
+
try {
|
|
534
|
+
// Revalidate immediately before the first credential/file/network seam.
|
|
535
|
+
this.trustedCwd(options);
|
|
536
|
+
// `maxTokens` is a host-local Agent Loop budget. The private Codex
|
|
537
|
+
// request has no max_output_tokens field, so do not hand this hint to
|
|
538
|
+
// the direct runner (which deliberately rejects unsupported controls).
|
|
539
|
+
const { maxTokens: hostMaxTokens, ...directInput } = options;
|
|
540
|
+
void hostMaxTokens;
|
|
541
|
+
const directOptions = {
|
|
542
|
+
...directInput,
|
|
543
|
+
model: options.model === 'default' ? this.config.codex.directModel : options.model,
|
|
544
|
+
signal: directSignal,
|
|
545
|
+
};
|
|
546
|
+
const attachments = this.getAttachments();
|
|
547
|
+
const chunks = this.runCodexDirect(directOptions, {
|
|
548
|
+
request: (body, requestSignal) => {
|
|
549
|
+
phase = 'stream';
|
|
550
|
+
promptSubmissionState = 'unknown';
|
|
551
|
+
return this.codexCredentials.requestResponses(body, requestSignal);
|
|
552
|
+
},
|
|
553
|
+
maxRequestBytes: this.config.codex.maxRequestBytes,
|
|
554
|
+
maxRequestImageBytes: this.config.codex.maxRequestImageBytes,
|
|
555
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
556
|
+
});
|
|
557
|
+
for await (const chunk of chunks) {
|
|
558
|
+
if (chunk.type === 'text-delta' && chunk.text.length > 0)
|
|
559
|
+
assistantTextForwarded = true;
|
|
560
|
+
yield chunk;
|
|
561
|
+
}
|
|
562
|
+
outcome = 'ok';
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
if (signal.aborted) {
|
|
567
|
+
outcome = 'aborted';
|
|
568
|
+
throw abortReason(signal);
|
|
569
|
+
}
|
|
570
|
+
if (error instanceof Error && error.cause === 'prompt-limit') {
|
|
571
|
+
phase = 'preflight';
|
|
572
|
+
promptSubmissionState = 'not-submitted';
|
|
573
|
+
outcome = 'preflight';
|
|
574
|
+
throw preflightFailure(error);
|
|
575
|
+
}
|
|
576
|
+
const failure = timeoutSignal.aborted && !signal.aborted
|
|
577
|
+
? new Error('Codex direct Responses request timed out', { cause: 'timeout' })
|
|
578
|
+
: error;
|
|
579
|
+
outcome = outcomeFor(failure);
|
|
580
|
+
throw directFailure(definition, failure);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Build and bound the argv locally before any auth probe or generation subprocess starts.
|
|
584
|
+
let invocation;
|
|
585
|
+
try {
|
|
586
|
+
invocation = buildInvocation(definition.cli, {
|
|
587
|
+
cwd,
|
|
588
|
+
prompt: buildPrompt(options, this.config.maxPromptBytes),
|
|
589
|
+
model: options.model,
|
|
590
|
+
...(definition.cli !== 'cursor' && options.reasoningEffort !== undefined
|
|
591
|
+
? { reasoningEffort: String(options.reasoningEffort) }
|
|
592
|
+
: {}),
|
|
593
|
+
maxTurns: profile.maxTurns,
|
|
594
|
+
command: profile.command,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
outcome = 'preflight';
|
|
599
|
+
throw preflightFailure(error);
|
|
600
|
+
}
|
|
601
|
+
phase = 'auth';
|
|
298
602
|
try {
|
|
299
603
|
await this.verifyAuth(definition.cli, {
|
|
300
604
|
command: profile.command,
|
|
301
|
-
cwd
|
|
605
|
+
cwd,
|
|
302
606
|
timeoutMs: this.config.authProbeTimeoutMs,
|
|
303
607
|
maxOutputBytes: this.config.maxAuthProbeBytes,
|
|
304
608
|
extraEnvNames: this.config.extraEnvNames,
|
|
@@ -317,24 +621,16 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
317
621
|
outcome = outcomeFor(error);
|
|
318
622
|
throw cliFailure(definition, profile.command, error);
|
|
319
623
|
}
|
|
320
|
-
//
|
|
624
|
+
// Revalidate the authority boundary after auth so a stale/replaced session
|
|
625
|
+
// cannot reach the generation process. Catalog discovery belongs exclusively
|
|
626
|
+
// to listModels and never delays a generation request.
|
|
321
627
|
phase = 'preflight';
|
|
322
|
-
let invocation;
|
|
323
628
|
try {
|
|
324
|
-
|
|
325
|
-
cwd: this.cwd,
|
|
326
|
-
prompt: buildPrompt(options, this.config.maxPromptBytes),
|
|
327
|
-
model: options.model,
|
|
328
|
-
...(definition.cli !== 'cursor' && options.reasoningEffort !== undefined
|
|
329
|
-
? { reasoningEffort: String(options.reasoningEffort) }
|
|
330
|
-
: {}),
|
|
331
|
-
maxTurns: profile.maxTurns,
|
|
332
|
-
command: profile.command,
|
|
333
|
-
});
|
|
629
|
+
cwd = this.trustedCwd(options);
|
|
334
630
|
}
|
|
335
631
|
catch (error) {
|
|
336
|
-
outcome =
|
|
337
|
-
throw error;
|
|
632
|
+
outcome = outcomeFor(error);
|
|
633
|
+
throw cliFailure(definition, profile.command, error);
|
|
338
634
|
}
|
|
339
635
|
const runnerOptions = {
|
|
340
636
|
timeoutMs: this.config.timeoutMs,
|
|
@@ -343,7 +639,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
343
639
|
maxOutputBytes: this.config.maxOutputBytes,
|
|
344
640
|
maxStderrBytes: this.config.maxStderrBytes,
|
|
345
641
|
extraEnvNames: this.config.extraEnvNames,
|
|
346
|
-
onDiagnostic: diagnostic => this.onDiagnostic?.(definition.route, diagnostic),
|
|
642
|
+
onDiagnostic: diagnostic => this.onDiagnostic?.(definition.route, redactDiagnostic(diagnostic)),
|
|
347
643
|
onSettled: context => { transportContext = context; },
|
|
348
644
|
signal,
|
|
349
645
|
};
|
|
@@ -357,6 +653,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
357
653
|
// Calling an injected runner may itself perform work or throw synchronously, so
|
|
358
654
|
// stop claiming not-submitted immediately before control crosses that boundary.
|
|
359
655
|
promptSubmissionState = 'unknown';
|
|
656
|
+
invocation = { ...invocation, cwd: this.trustedCwd(options) };
|
|
360
657
|
const deltas = this.runText(invocation, runnerOptions);
|
|
361
658
|
for await (const delta of deltas) {
|
|
362
659
|
if (delta.length === 0)
|
|
@@ -380,11 +677,12 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
|
|
|
380
677
|
}
|
|
381
678
|
/** Abort every active/future subprocess when the owning Cordis fiber unloads. */
|
|
382
679
|
shutdown() {
|
|
383
|
-
this.codexCatalogCache = undefined;
|
|
384
|
-
this.subscriptionCatalogCache.clear();
|
|
385
680
|
if (!this.lifecycle.signal.aborted) {
|
|
386
681
|
this.lifecycle.abort(new Error('coding subscription provider unloaded', { cause: 'abort' }));
|
|
387
682
|
}
|
|
683
|
+
this.codexCatalogCache = undefined;
|
|
684
|
+
this.subscriptionCatalogCache.clear();
|
|
685
|
+
this.catalogRefreshes.clear();
|
|
388
686
|
}
|
|
389
687
|
}
|
|
390
688
|
export function enabledRoutes(config) {
|