praxis-agent 0.69.0 → 0.70.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/README.md +63 -4
- package/dist/build-identity.json +1 -1
- package/dist/cli-runtime.js +7 -1
- package/dist/evals/eval-tool-admission.d.ts +9 -0
- package/dist/evals/eval-tool-admission.js +33 -0
- package/dist/evals/held-out-corpus.d.ts +2 -2
- package/dist/evals/held-out-corpus.js +19 -8
- package/dist/evals/held-out-qualification.js +8 -3
- package/dist/providers/codex-responses.d.ts +26 -0
- package/dist/providers/codex-responses.js +155 -0
- package/dist/providers/codex-subscription.js +1 -0
- package/dist/providers/provider-registry.js +24 -1
- package/dist/providers/provider-settings.d.ts +1 -1
- package/dist/providers/provider-settings.js +23 -11
- package/dist/providers/responses-codec.d.ts +3 -0
- package/dist/providers/responses-codec.js +13 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -88,6 +88,11 @@ This uses an undocumented third-party subscription/backend contract and may
|
|
|
88
88
|
change; it is not Claude subscription authentication. Subscription runs retain
|
|
89
89
|
token usage but do not provide API-dollar cost or enforce USD budgets.
|
|
90
90
|
|
|
91
|
+
Private relays can use the explicitly opted-in experimental custom
|
|
92
|
+
`codex-responses` protocol with an API-key credential and configured model and
|
|
93
|
+
URL. It has subscription billing semantics, no built-in relay/default, and no
|
|
94
|
+
automatic protocol fallback; see [Getting Started](docs/GETTING_STARTED.md).
|
|
95
|
+
|
|
91
96
|
For Anthropic Messages:
|
|
92
97
|
|
|
93
98
|
```sh
|
|
@@ -143,14 +148,41 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
143
148
|
evidence produces null deltas, while the gate requires no pass-rate or
|
|
144
149
|
safety-rate regression, every candidate verifier to be satisfied, and every
|
|
145
150
|
candidate `high` or `release` task to pass, and rejects incomplete safety
|
|
146
|
-
evidence.
|
|
151
|
+
evidence. Eval `allowed_tools` controls catalog exposure; in the default
|
|
152
|
+
Project/Plugin Eval runtime, allowed non-Bash tools remain preapproved, while
|
|
153
|
+
Bash preapproval requires existing static semantic and path checks to keep
|
|
154
|
+
recognized paths inside the isolated workspace or explicit added
|
|
155
|
+
directories. Recognized outside-root commands such as `find /` fail the
|
|
156
|
+
permission boundary before process execution; effects hidden inside opaque
|
|
157
|
+
executables are not part of this guarantee.
|
|
147
158
|
- **Held-out qualification** — The explicit `praxis eval qualify` surface
|
|
148
159
|
preflights a pinned provider,
|
|
149
160
|
profile, and model, then runs the immutable held-out corpus as 36 local
|
|
150
161
|
Project Eval runs. Baseline-only results use `qualified: null`; candidate
|
|
151
162
|
claims require complete safe, comparable evidence, and unknown usage/cost
|
|
152
|
-
keeps optimization claims disabled.
|
|
153
|
-
|
|
163
|
+
keeps optimization claims disabled. Loading and qualification accept safe
|
|
164
|
+
matching versioned `praxis-held-out-vN` identities. V1 is unchanged; v2 is
|
|
165
|
+
frozen at 3 repositories, 12 tasks, and 36 runs with digest
|
|
166
|
+
`sha256:1ae6e3485684db143ead1983479500f7fb80d13fd99769d8e202d4c7c35881b3`.
|
|
167
|
+
The preserved v2 real-provider campaign pins
|
|
168
|
+
`openai-responses/default/openai-responses/gpt-5.4-mini` and records baseline
|
|
169
|
+
36/36 completed, 32/36 behavior, 36/36 mutation-oriented safety, 32/36
|
|
170
|
+
verifier satisfied, and `qualified: null`; candidate 36/36 completed, 30/36
|
|
171
|
+
behavior, 36/36 mutation-oriented safety, 30/36 verifier satisfied, and
|
|
172
|
+
`qualified: false`. Usage was known for 35/36 baseline and 34/36 candidate
|
|
173
|
+
runs, while cost was unknown for all runs. This aggregate establishes no
|
|
174
|
+
quality, broad security, cost, latency, or efficiency claim. The −5.56-point
|
|
175
|
+
pass-rate delta leaves Task 8.2/Phase 9 locked. Any result-informed
|
|
176
|
+
remediation or requalification requires a new v3 held-out corpus.
|
|
177
|
+
[Issue #722](https://github.com/Forest-Isle/Praxis/issues/722) now freezes the
|
|
178
|
+
fresh `praxis-held-out-v3` corpus for
|
|
179
|
+
`frame-codec`, `graph-craft`, and `route-forge`: 12 repository-qualified
|
|
180
|
+
tasks, 3 repetitions each, and 36 planned runs with digest
|
|
181
|
+
`sha256:9380f5ccd9b920bf9767381f2d36d91dc04abe645db0a7c1a5f1597279d579ff`.
|
|
182
|
+
It is frozen before execution, has not been provider-executed, supplies no
|
|
183
|
+
baseline, candidate, or qualification evidence, and does not complete Task
|
|
184
|
+
8.2 or unlock Phase 9. A bounded DeepSeek baseline for the exact
|
|
185
|
+
`anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
|
|
154
186
|
33/36 passes (91.7%) and 36/36 safety passes. The
|
|
155
187
|
`config-kit.add-json-output` run 3 failed closed with provider error
|
|
156
188
|
`Provider reported max_tokens with completed tool calls`. The
|
|
@@ -163,7 +195,34 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
163
195
|
The 33-run known-cost subtotal is USD 0.068322756, not a total. This
|
|
164
196
|
measures only that pinned 32,768-context/4,096-output configuration;
|
|
165
197
|
baseline-only evidence remains `qualified: null`, and no live-model quality
|
|
166
|
-
or optimization claim is established.
|
|
198
|
+
or optimization claim is established. A single preserved candidate run with
|
|
199
|
+
matching comparison-critical identities and a matching emitted-runtime
|
|
200
|
+
artifact digest completed 36/36 runs with 35/36 behavior (97.2%), 36/36
|
|
201
|
+
mutation-oriented safety checks, and 35/36 required-verifier runs. It is
|
|
202
|
+
`qualified: false`: the baseline-passing
|
|
203
|
+
`string-kit.add-middle-truncate` run 1 timed out after 180,105 ms when the
|
|
204
|
+
model issued a host-wide `find /` Bash command; it made no workspace
|
|
205
|
+
mutation, the existing mutation-oriented safety checks still passed, usage/
|
|
206
|
+
cost is unknown, and the verifier did not run. This is a coding-policy/tool-
|
|
207
|
+
admission failure, not a provider transport failure or a broader security
|
|
208
|
+
claim. The candidate's 35-run known-cost subtotal is USD 0.075007728, not a
|
|
209
|
+
total; cost delta is unavailable and optimization remains false because both
|
|
210
|
+
evidence sets contain unknown runs. Candidate median/p95 turns were 6/9 and
|
|
211
|
+
median/p95 duration was 22,938.5/54,918 ms; versus baseline, deltas were
|
|
212
|
+
+1/+2 turns and -5,715/+2,721 ms. These mixed turn/duration deltas are
|
|
213
|
+
evidence only, not an improvement claim. The candidate is not rerun or selected away;
|
|
214
|
+
Task 8.2 remains incomplete and Phase 9 stays locked. Any result-informed
|
|
215
|
+
remediation or requalification first requires a new held-out corpus version.
|
|
216
|
+
The subsequent bounded eval-admission fix does not change or select away this
|
|
217
|
+
preserved result.
|
|
218
|
+
Issue #724's one predeclared no-tool/no-file smoke for the pinned
|
|
219
|
+
`openai-responses/default/openai-responses/gpt-5.5` configuration returned
|
|
220
|
+
HTTP 404 after 2,995 ms before any v3 corpus run. Relay health and model-list
|
|
221
|
+
reads were healthy and advertised `gpt-5.5`, but the 404 source is unknown;
|
|
222
|
+
no v3 corpus baseline/candidate result, qualification, pass/safety/verifier/
|
|
223
|
+
cost/latency/model-quality evidence exists, and the no-retry/no-substitution
|
|
224
|
+
rule was honored.
|
|
225
|
+
Task 8.2 remains incomplete and Phase 9 stays locked.
|
|
167
226
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
168
227
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
169
228
|
composer grammar, compact stable tool rows, responsive density,
|
package/dist/build-identity.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schema_version":"1.0","source_revision":"git:
|
|
1
|
+
{"schema_version":"1.0","source_revision":"git:513540d96cd0c8f765708139a232cbb33c29cdcb","source_dirty":false,"artifact_sha256":"sha256:989b6c63e65c1452000a0225327eaf3fe6b37c2c6645aa8a29df5dff9ab74885"}
|
package/dist/cli-runtime.js
CHANGED
|
@@ -72,6 +72,7 @@ import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaude
|
|
|
72
72
|
import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
|
|
73
73
|
import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
|
|
74
74
|
import { executeProjectEvalCommand, PROJECT_EVAL_HELP, } from './evals/project-eval.js';
|
|
75
|
+
import { isEvalToolCallPreapproved } from './evals/eval-tool-admission.js';
|
|
75
76
|
import { PROJECT_EVAL_COMPARE_HELP } from './evals/project-eval-comparison.js';
|
|
76
77
|
import { loadPraxisBuildIdentity } from './platform/praxis-build-identity.js';
|
|
77
78
|
import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
|
|
@@ -2323,7 +2324,12 @@ const defaultPluginEvalRuntimeFactory = {
|
|
|
2323
2324
|
USERPROFILE: options.home,
|
|
2324
2325
|
},
|
|
2325
2326
|
providerEnvironment: process.env,
|
|
2326
|
-
isSessionActionApproved: (call) =>
|
|
2327
|
+
isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
|
|
2328
|
+
cwd: options.cwd,
|
|
2329
|
+
homeDirectory: options.home,
|
|
2330
|
+
allowedTools: options.allowedTools,
|
|
2331
|
+
additionalDirectories: options.addDirs,
|
|
2332
|
+
}),
|
|
2327
2333
|
controls: {
|
|
2328
2334
|
...DEFAULT_CLI_CONTROLS,
|
|
2329
2335
|
dataPlane: options.dataPlane,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModelToolCall } from '../core/runtime.js';
|
|
2
|
+
export interface EvalToolAdmissionOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
homeDirectory: string;
|
|
5
|
+
allowedTools: readonly string[];
|
|
6
|
+
additionalDirectories: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function isEvalToolCallPreapproved(call: ModelToolCall, options: EvalToolAdmissionOptions): boolean;
|
|
9
|
+
//# sourceMappingURL=eval-tool-admission.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { validateBashSemantics } from '../permissions/bash-ast.js';
|
|
3
|
+
import { validateBashPathSafety } from '../permissions/bash-path-safety.js';
|
|
4
|
+
function commandInput(input) {
|
|
5
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
6
|
+
return;
|
|
7
|
+
const command = input.command;
|
|
8
|
+
return typeof command === 'string' ? command : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function isEvalToolCallPreapproved(call, options) {
|
|
11
|
+
if (!options.allowedTools.includes(call.name))
|
|
12
|
+
return false;
|
|
13
|
+
if (call.name !== 'Bash')
|
|
14
|
+
return true;
|
|
15
|
+
const command = commandInput(call.input);
|
|
16
|
+
if (command === undefined)
|
|
17
|
+
return false;
|
|
18
|
+
if (!validateBashSemantics(command).safe)
|
|
19
|
+
return false;
|
|
20
|
+
const cwd = resolve(options.cwd);
|
|
21
|
+
const roots = [
|
|
22
|
+
cwd,
|
|
23
|
+
...options.additionalDirectories.map((directory) => resolve(cwd, directory)),
|
|
24
|
+
];
|
|
25
|
+
return validateBashPathSafety(command, {
|
|
26
|
+
cwd,
|
|
27
|
+
homeDirectory: options.homeDirectory,
|
|
28
|
+
readRoots: roots,
|
|
29
|
+
writeRoots: roots,
|
|
30
|
+
permissionMode: 'acceptEdits',
|
|
31
|
+
}).safe;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=eval-tool-admission.js.map
|
|
@@ -14,8 +14,8 @@ export interface HeldOutCorpusRepository {
|
|
|
14
14
|
export interface HeldOutCorpus {
|
|
15
15
|
readonly root: string;
|
|
16
16
|
readonly schemaVersion: '1.0';
|
|
17
|
-
readonly id:
|
|
18
|
-
readonly version:
|
|
17
|
+
readonly id: `praxis-held-out-v${number}`;
|
|
18
|
+
readonly version: number;
|
|
19
19
|
readonly split: 'held-out';
|
|
20
20
|
readonly repetitions: 3;
|
|
21
21
|
readonly policy: HeldOutCorpusPolicy;
|
|
@@ -8,7 +8,6 @@ const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
|
8
8
|
const MAX_FILES = 4096;
|
|
9
9
|
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
10
10
|
const MAX_ENTRIES = 16_384;
|
|
11
|
-
const REQUIRED_TAGS = ['held-out', 'praxis-held-out-v1'];
|
|
12
11
|
const FORBIDDEN_TAGS = new Set([
|
|
13
12
|
'tuning',
|
|
14
13
|
'calibration',
|
|
@@ -22,6 +21,20 @@ const MUTATION_GLOB_OPTIONS = {
|
|
|
22
21
|
nocomment: true,
|
|
23
22
|
nonegate: true,
|
|
24
23
|
};
|
|
24
|
+
function validateCorpusIdentity(id, version) {
|
|
25
|
+
if (typeof id !== 'string' || !/^praxis-held-out-v[1-9]\d*$/u.test(id))
|
|
26
|
+
throw new Error('Unsupported corpus id');
|
|
27
|
+
if (typeof version !== 'number' ||
|
|
28
|
+
!Number.isSafeInteger(version) ||
|
|
29
|
+
version <= 0)
|
|
30
|
+
throw new Error('Unsupported corpus version');
|
|
31
|
+
if (id === 'praxis-held-out-v1' && version !== 1)
|
|
32
|
+
throw new Error('Unsupported corpus version');
|
|
33
|
+
const suffix = Number(id.slice('praxis-held-out-v'.length));
|
|
34
|
+
if (!Number.isSafeInteger(suffix) || suffix !== version)
|
|
35
|
+
throw new Error('Corpus id and version do not match');
|
|
36
|
+
return { id: id, version };
|
|
37
|
+
}
|
|
25
38
|
function object(value, label) {
|
|
26
39
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
27
40
|
throw new Error(`${label} must be an object`);
|
|
@@ -175,10 +188,7 @@ export async function loadHeldOutCorpus(root) {
|
|
|
175
188
|
], 'corpus');
|
|
176
189
|
if (manifest.schema_version !== '1.0')
|
|
177
190
|
throw new Error('Unsupported corpus schema_version');
|
|
178
|
-
|
|
179
|
-
throw new Error('Unsupported corpus id');
|
|
180
|
-
if (manifest.version !== 1)
|
|
181
|
-
throw new Error('Unsupported corpus version');
|
|
191
|
+
const identity = validateCorpusIdentity(manifest.id, manifest.version);
|
|
182
192
|
if (manifest.split !== 'held-out')
|
|
183
193
|
throw new Error('corpus split must be held-out');
|
|
184
194
|
if (manifest.repetitions !== 3)
|
|
@@ -270,7 +280,8 @@ export async function loadHeldOutCorpus(root) {
|
|
|
270
280
|
const forbidden = item.expect.forbiddenChangedPaths.map((pattern) => new Minimatch(pattern, MUTATION_GLOB_OPTIONS));
|
|
271
281
|
if (mutationPaths.some((path) => forbidden.some((matcher) => matcher.match(path))))
|
|
272
282
|
throw new Error(`${item.name} mutation paths overlap`);
|
|
273
|
-
if (!
|
|
283
|
+
if (!item.tags.includes('held-out') ||
|
|
284
|
+
!item.tags.includes(identity.id) ||
|
|
274
285
|
!item.tags.includes(repository.id))
|
|
275
286
|
throw new Error(`${item.name} is missing required held-out tags`);
|
|
276
287
|
if (item.tags.some((tag) => FORBIDDEN_TAGS.has(tag)))
|
|
@@ -285,8 +296,8 @@ export async function loadHeldOutCorpus(root) {
|
|
|
285
296
|
return {
|
|
286
297
|
root: corpusRoot,
|
|
287
298
|
schemaVersion: '1.0',
|
|
288
|
-
id:
|
|
289
|
-
version:
|
|
299
|
+
id: identity.id,
|
|
300
|
+
version: identity.version,
|
|
290
301
|
split: 'held-out',
|
|
291
302
|
repetitions: 3,
|
|
292
303
|
policy: {
|
|
@@ -140,7 +140,7 @@ export function parseHeldOutQualificationOptions(argv) {
|
|
|
140
140
|
throw new Error('--profile is not a safe eval identifier');
|
|
141
141
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$/u.test(options.model ?? ''))
|
|
142
142
|
throw new Error('--model is not a safe eval identifier');
|
|
143
|
-
if (!/^praxis-held-out-
|
|
143
|
+
if (!/^praxis-held-out-v[1-9]\d*@sha256:[0-9a-f]{64}$/u.test(options.confirmHeldOut ?? ''))
|
|
144
144
|
throw new Error('--confirm-held-out must be <id>@sha256:<64 lowercase hex>');
|
|
145
145
|
return options;
|
|
146
146
|
}
|
|
@@ -639,8 +639,13 @@ function validateQualificationResult(value) {
|
|
|
639
639
|
Array.isArray(corpusObject) ||
|
|
640
640
|
Object.keys(corpusObject).length !== corpusKeys.length ||
|
|
641
641
|
Object.keys(corpusObject).some((key) => !corpusKeys.includes(key)) ||
|
|
642
|
-
corpusObject.id !== '
|
|
643
|
-
corpusObject.
|
|
642
|
+
typeof corpusObject.id !== 'string' ||
|
|
643
|
+
!/^praxis-held-out-v[1-9]\d*$/u.test(corpusObject.id) ||
|
|
644
|
+
typeof corpusObject.version !== 'number' ||
|
|
645
|
+
!Number.isSafeInteger(corpusObject.version) ||
|
|
646
|
+
corpusObject.version <= 0 ||
|
|
647
|
+
Number(corpusObject.id.slice('praxis-held-out-v'.length)) !==
|
|
648
|
+
corpusObject.version ||
|
|
644
649
|
corpusObject.repetitions !== 3 ||
|
|
645
650
|
!DIGEST.test(String(corpusObject.content_sha256)))
|
|
646
651
|
throw new Error('Qualification corpus fields are invalid');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ModelProvider, type ModelRequest, type ModelStreamEvent, type ModelThinkingConfig } from '../core/runtime.js';
|
|
2
|
+
export interface CodexResponsesProviderOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
contextWindowTokens?: number;
|
|
7
|
+
thinking?: ModelThinkingConfig;
|
|
8
|
+
maxStreamBufferBytes?: number;
|
|
9
|
+
maxToolArgumentsBytes?: number;
|
|
10
|
+
maxToolCallsPerResponse?: number;
|
|
11
|
+
maxToolMetadataBytes?: number;
|
|
12
|
+
maxReasoningBytes?: number;
|
|
13
|
+
maxErrorBodyBytes?: number;
|
|
14
|
+
fetchImplementation?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export declare class CodexResponsesProvider implements ModelProvider {
|
|
17
|
+
private readonly options;
|
|
18
|
+
readonly model: string;
|
|
19
|
+
readonly capabilities: ModelProvider['capabilities'];
|
|
20
|
+
private readonly fetchImplementation;
|
|
21
|
+
private readonly codec;
|
|
22
|
+
private readonly maxErrorBodyBytes;
|
|
23
|
+
constructor(options: CodexResponsesProviderOptions);
|
|
24
|
+
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=codex-responses.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
import { ResponsesCodec } from './responses-codec.js';
|
|
3
|
+
import { reportProviderTransportActivity } from './provider-transport-activity.js';
|
|
4
|
+
function failure(kind, retryable, status) {
|
|
5
|
+
return new ModelProviderError(status === undefined
|
|
6
|
+
? 'Codex Responses provider request failed'
|
|
7
|
+
: `Codex Responses provider request failed with HTTP ${status}`, { kind, retryable, ...(status === undefined ? {} : { status }) });
|
|
8
|
+
}
|
|
9
|
+
function statusFailure(status) {
|
|
10
|
+
if (status === 401 || status === 403)
|
|
11
|
+
return failure('authentication_failed', false, status);
|
|
12
|
+
if (status === 402)
|
|
13
|
+
return failure('billing_error', false, status);
|
|
14
|
+
if (status === 408)
|
|
15
|
+
return failure('timeout', true, status);
|
|
16
|
+
if (status === 429)
|
|
17
|
+
return failure('rate_limit', true, status);
|
|
18
|
+
if (status === 529)
|
|
19
|
+
return failure('overloaded', true, status);
|
|
20
|
+
if (status >= 500)
|
|
21
|
+
return failure('server_error', true, status);
|
|
22
|
+
return failure('invalid_request', false, status);
|
|
23
|
+
}
|
|
24
|
+
export class CodexResponsesProvider {
|
|
25
|
+
options;
|
|
26
|
+
model;
|
|
27
|
+
capabilities;
|
|
28
|
+
fetchImplementation;
|
|
29
|
+
codec;
|
|
30
|
+
maxErrorBodyBytes;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.model = options.model;
|
|
34
|
+
this.fetchImplementation = options.fetchImplementation ?? fetch;
|
|
35
|
+
this.maxErrorBodyBytes = options.maxErrorBodyBytes ?? 64 * 1024;
|
|
36
|
+
if (!Number.isSafeInteger(this.maxErrorBodyBytes) ||
|
|
37
|
+
this.maxErrorBodyBytes <= 0)
|
|
38
|
+
throw new Error('Codex Responses provider limits must be positive integers');
|
|
39
|
+
this.codec = new ResponsesCodec({
|
|
40
|
+
providerLabel: 'Codex Responses provider',
|
|
41
|
+
requestDialect: 'codex-native',
|
|
42
|
+
...(options.thinking === undefined ? {} : { thinking: options.thinking }),
|
|
43
|
+
...(options.maxStreamBufferBytes === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { maxStreamBufferBytes: options.maxStreamBufferBytes }),
|
|
46
|
+
...(options.maxToolArgumentsBytes === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { maxToolArgumentsBytes: options.maxToolArgumentsBytes }),
|
|
49
|
+
...(options.maxToolCallsPerResponse === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: { maxToolCallsPerResponse: options.maxToolCallsPerResponse }),
|
|
52
|
+
...(options.maxToolMetadataBytes === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { maxToolMetadataBytes: options.maxToolMetadataBytes }),
|
|
55
|
+
...(options.maxReasoningBytes === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { maxReasoningBytes: options.maxReasoningBytes }),
|
|
58
|
+
});
|
|
59
|
+
this.capabilities = {
|
|
60
|
+
streaming: true,
|
|
61
|
+
usage: true,
|
|
62
|
+
tools: true,
|
|
63
|
+
images: true,
|
|
64
|
+
documents: false,
|
|
65
|
+
webSearch: false,
|
|
66
|
+
thinking: {
|
|
67
|
+
modes: ['disabled', 'enabled', 'adaptive'],
|
|
68
|
+
maxTokens: false,
|
|
69
|
+
},
|
|
70
|
+
...(options.contextWindowTokens === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { contextWindowTokens: options.contextWindowTokens }),
|
|
73
|
+
terminalReasons: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async *complete(request) {
|
|
77
|
+
const body = this.codec.serialize(request, this.model);
|
|
78
|
+
const signal = request.signal;
|
|
79
|
+
let response;
|
|
80
|
+
try {
|
|
81
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
82
|
+
response = await this.fetchImplementation(`${this.options.baseUrl.replace(/\/+$/u, '')}/responses`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
86
|
+
accept: 'text/event-stream',
|
|
87
|
+
'content-type': 'application/json',
|
|
88
|
+
originator: 'praxis',
|
|
89
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
...(signal ? { signal } : {}),
|
|
93
|
+
});
|
|
94
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (signal?.aborted)
|
|
98
|
+
throw failure('cancelled', false);
|
|
99
|
+
if (typeof error === 'object' &&
|
|
100
|
+
error !== null &&
|
|
101
|
+
'name' in error &&
|
|
102
|
+
error.name === 'TimeoutError')
|
|
103
|
+
throw failure('timeout', true);
|
|
104
|
+
if (typeof error === 'object' &&
|
|
105
|
+
error !== null &&
|
|
106
|
+
'code' in error &&
|
|
107
|
+
error.code === 'ETIMEDOUT')
|
|
108
|
+
throw failure('timeout', true);
|
|
109
|
+
throw failure('transport_error', true);
|
|
110
|
+
}
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const reader = response.body?.getReader();
|
|
113
|
+
if (reader) {
|
|
114
|
+
let total = 0;
|
|
115
|
+
let ended = false;
|
|
116
|
+
try {
|
|
117
|
+
while (true) {
|
|
118
|
+
const next = await reader.read();
|
|
119
|
+
if (next.done) {
|
|
120
|
+
ended = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (next.value.byteLength > 0)
|
|
124
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
125
|
+
total += next.value.byteLength;
|
|
126
|
+
if (total > this.maxErrorBodyBytes)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* preserve the redacted HTTP status classification */
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (!ended) {
|
|
135
|
+
try {
|
|
136
|
+
await reader.cancel();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* preserve the redacted HTTP status classification */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
reader.releaseLock();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw statusFailure(response.status);
|
|
146
|
+
}
|
|
147
|
+
if (!response.body)
|
|
148
|
+
throw failure('transport_error', true);
|
|
149
|
+
yield* this.codec.stream(response.body, {
|
|
150
|
+
...(signal === undefined ? {} : { signal }),
|
|
151
|
+
onChunk: () => reportProviderTransportActivity(request, 'response-chunk'),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=codex-responses.js.map
|
|
@@ -104,6 +104,7 @@ export class CodexSubscriptionProvider {
|
|
|
104
104
|
this.maxErrorBodyBytes = positiveInteger(options.maxErrorBodyBytes, DEFAULT_MAX_ERROR);
|
|
105
105
|
this.responsesCodec = new ResponsesCodec({
|
|
106
106
|
providerLabel: 'Codex subscription provider',
|
|
107
|
+
requestDialect: 'codex-native',
|
|
107
108
|
...(options.thinking === undefined ? {} : { thinking: options.thinking }),
|
|
108
109
|
...(options.maxStreamBufferBytes === undefined
|
|
109
110
|
? {}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnthropicCompatibleProvider } from './anthropic-compatible.js';
|
|
2
2
|
import { OpenAICompatibleProvider } from './openai-compatible.js';
|
|
3
3
|
import { OpenAIResponsesProvider } from './openai-responses.js';
|
|
4
|
+
import { CodexResponsesProvider } from './codex-responses.js';
|
|
4
5
|
import { CodexSubscriptionProvider } from './codex-subscription.js';
|
|
5
6
|
import { DeadlineModelProvider } from './deadline-provider.js';
|
|
6
7
|
import { NonStreamingFallbackModelProvider } from './non-streaming-fallback-provider.js';
|
|
@@ -41,7 +42,8 @@ export function resolveProviderContextWindowTokens(options) {
|
|
|
41
42
|
if (options.protocol === 'anthropic-messages')
|
|
42
43
|
return resolveAnthropicModelSpec(options.modelId, options.explicitContextWindowTokens).contextWindowTokens;
|
|
43
44
|
if (options.protocol === 'openai-compatible' ||
|
|
44
|
-
options.protocol === 'openai-responses'
|
|
45
|
+
options.protocol === 'openai-responses' ||
|
|
46
|
+
options.protocol === 'codex-responses')
|
|
45
47
|
return options.explicitContextWindowTokens;
|
|
46
48
|
return undefined;
|
|
47
49
|
}
|
|
@@ -191,6 +193,27 @@ class NativeProviderRegistry {
|
|
|
191
193
|
: { fetchImplementation: this.options.fetchImplementation }),
|
|
192
194
|
}));
|
|
193
195
|
}
|
|
196
|
+
if (target.protocol === 'codex-responses') {
|
|
197
|
+
if (this.options.credential.type !== 'api-key') {
|
|
198
|
+
throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: an API key is required');
|
|
199
|
+
}
|
|
200
|
+
return this.withDeadline(new CodexResponsesProvider({
|
|
201
|
+
baseUrl: target.baseUrl,
|
|
202
|
+
model: target.modelId,
|
|
203
|
+
apiKey: this.options.credential.secret,
|
|
204
|
+
...(this.options.context?.contextWindowTokens === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: {
|
|
207
|
+
contextWindowTokens: this.options.context.contextWindowTokens,
|
|
208
|
+
}),
|
|
209
|
+
...(this.options.codexThinking === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { thinking: this.options.codexThinking }),
|
|
212
|
+
...(this.options.fetchImplementation === undefined
|
|
213
|
+
? {}
|
|
214
|
+
: { fetchImplementation: this.options.fetchImplementation }),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
194
217
|
if (target.protocol === 'anthropic-messages') {
|
|
195
218
|
if (this.options.credential.type !== 'api-key') {
|
|
196
219
|
throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: an API key is required');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderProtocol = 'openai-compatible' | 'openai-responses' | 'anthropic-messages' | 'codex-subscription';
|
|
1
|
+
export type ProviderProtocol = 'openai-compatible' | 'openai-responses' | 'anthropic-messages' | 'codex-subscription' | 'codex-responses';
|
|
2
2
|
export type CredentialSource = {
|
|
3
3
|
source: 'env';
|
|
4
4
|
name: string;
|
|
@@ -160,8 +160,9 @@ function parseProvider(value, providerId) {
|
|
|
160
160
|
const protocol = value.protocol;
|
|
161
161
|
if (protocol !== 'openai-compatible' &&
|
|
162
162
|
protocol !== 'openai-responses' &&
|
|
163
|
-
protocol !== 'anthropic-messages'
|
|
164
|
-
|
|
163
|
+
protocol !== 'anthropic-messages' &&
|
|
164
|
+
protocol !== 'codex-responses') {
|
|
165
|
+
fail(`providers.${providerId}.protocol must be openai-compatible, openai-responses, anthropic-messages, or codex-responses`);
|
|
165
166
|
}
|
|
166
167
|
if (!isRecord(value.profiles) || Object.keys(value.profiles).length === 0)
|
|
167
168
|
fail(`providers.${providerId}.profiles must be a non-empty object`);
|
|
@@ -219,12 +220,19 @@ function mergeSelection(...values) {
|
|
|
219
220
|
}
|
|
220
221
|
function validateExperimental(value) {
|
|
221
222
|
if (value === undefined)
|
|
222
|
-
return false;
|
|
223
|
-
if (!isRecord(value)
|
|
224
|
-
(
|
|
225
|
-
|
|
223
|
+
return { codexSubscription: false, codexResponses: false };
|
|
224
|
+
if (!isRecord(value))
|
|
225
|
+
fail('experimental.codexSubscription must be a boolean');
|
|
226
|
+
if (value.codexSubscription !== undefined &&
|
|
227
|
+
typeof value.codexSubscription !== 'boolean')
|
|
226
228
|
fail('experimental.codexSubscription must be a boolean');
|
|
227
|
-
|
|
229
|
+
if (value.codexResponses !== undefined &&
|
|
230
|
+
typeof value.codexResponses !== 'boolean')
|
|
231
|
+
fail('experimental.codexResponses must be a boolean');
|
|
232
|
+
return {
|
|
233
|
+
codexSubscription: value.codexSubscription === true,
|
|
234
|
+
codexResponses: value.codexResponses === true,
|
|
235
|
+
};
|
|
228
236
|
}
|
|
229
237
|
function ensureSelectionPart(value, field) {
|
|
230
238
|
return identifier(value, field);
|
|
@@ -246,7 +254,7 @@ export async function resolveProviderTarget(options) {
|
|
|
246
254
|
: {};
|
|
247
255
|
scanForPlaintextSecrets(user.providers, 'providers');
|
|
248
256
|
const userProviders = parseProviders(user.providers);
|
|
249
|
-
const
|
|
257
|
+
const experimental = validateExperimental(user.experimental);
|
|
250
258
|
const explicit = {
|
|
251
259
|
...(options.provider === undefined
|
|
252
260
|
? {}
|
|
@@ -287,7 +295,7 @@ export async function resolveProviderTarget(options) {
|
|
|
287
295
|
const selected = mergeSelection(settingsSelection, projectSelection, localSelection, envSelection, explicit);
|
|
288
296
|
const providerId = selected.provider ?? 'openai';
|
|
289
297
|
identifier(providerId, 'provider ID');
|
|
290
|
-
if (providerId === 'openai-codex' && !
|
|
298
|
+
if (providerId === 'openai-codex' && !experimental.codexSubscription)
|
|
291
299
|
fail('openai-codex requires experimental.codexSubscription=true');
|
|
292
300
|
if (providerId !== 'openai-codex' &&
|
|
293
301
|
selected.profile === undefined &&
|
|
@@ -303,6 +311,8 @@ export async function resolveProviderTarget(options) {
|
|
|
303
311
|
fail(`built-in provider ${providerId} has an incompatible protocol`);
|
|
304
312
|
if (custom?.protocol === 'codex-subscription')
|
|
305
313
|
fail('custom codex-subscription providers are not allowed');
|
|
314
|
+
if (custom?.protocol === 'codex-responses' && !experimental.codexResponses)
|
|
315
|
+
fail('custom codex-responses providers require experimental.codexResponses=true');
|
|
306
316
|
const definition = custom ?? BUILT_INS[providerId];
|
|
307
317
|
if (!definition)
|
|
308
318
|
throw new ProviderSettingsError('unknown_provider', `Invalid provider settings: unknown provider ${providerId}`);
|
|
@@ -339,8 +349,10 @@ export async function resolveProviderTarget(options) {
|
|
|
339
349
|
protocol,
|
|
340
350
|
baseUrl,
|
|
341
351
|
credential: resolvedCredential,
|
|
342
|
-
billingMode: protocol === 'codex-subscription'
|
|
343
|
-
|
|
352
|
+
billingMode: protocol === 'codex-subscription' || protocol === 'codex-responses'
|
|
353
|
+
? 'subscription'
|
|
354
|
+
: 'api',
|
|
355
|
+
experimental: providerId === 'openai-codex' || protocol === 'codex-responses',
|
|
344
356
|
};
|
|
345
357
|
}
|
|
346
358
|
//# sourceMappingURL=provider-settings.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ModelRequest, type ModelStreamEvent, type ModelThinkingConfig } from '../core/runtime.js';
|
|
2
|
+
export type ResponsesRequestDialect = 'public' | 'codex-native';
|
|
2
3
|
export interface ResponsesCodecOptions {
|
|
3
4
|
providerLabel?: string;
|
|
4
5
|
thinking?: ModelThinkingConfig;
|
|
@@ -7,6 +8,7 @@ export interface ResponsesCodecOptions {
|
|
|
7
8
|
maxToolCallsPerResponse?: number;
|
|
8
9
|
maxToolMetadataBytes?: number;
|
|
9
10
|
maxReasoningBytes?: number;
|
|
11
|
+
requestDialect?: ResponsesRequestDialect;
|
|
10
12
|
}
|
|
11
13
|
export interface ResponsesStreamOptions {
|
|
12
14
|
signal?: AbortSignal;
|
|
@@ -20,6 +22,7 @@ export declare class ResponsesCodec {
|
|
|
20
22
|
readonly maxReasoningBytes: number;
|
|
21
23
|
private readonly label;
|
|
22
24
|
private readonly configuredThinking;
|
|
25
|
+
private readonly requestDialect;
|
|
23
26
|
constructor(options?: ResponsesCodecOptions);
|
|
24
27
|
serialize(request: ModelRequest, model: string): Record<string, unknown>;
|
|
25
28
|
stream(body: ReadableStream<Uint8Array>, options?: ResponsesStreamOptions): AsyncIterable<ModelStreamEvent>;
|
|
@@ -123,7 +123,7 @@ function serializeMessages(messages) {
|
|
|
123
123
|
}
|
|
124
124
|
return { instructions: instructions.join('\n\n'), input };
|
|
125
125
|
}
|
|
126
|
-
function serializeResponsesRequest(request, model, configuredThinking) {
|
|
126
|
+
function serializeResponsesRequest(request, model, configuredThinking, dialect = 'public') {
|
|
127
127
|
if (request.webSearch !== undefined)
|
|
128
128
|
throw invalid('Codex subscription provider does not support web search');
|
|
129
129
|
if (request.betas !== undefined)
|
|
@@ -149,6 +149,15 @@ function serializeResponsesRequest(request, model, configuredThinking) {
|
|
|
149
149
|
instructions,
|
|
150
150
|
input,
|
|
151
151
|
};
|
|
152
|
+
if (dialect === 'codex-native') {
|
|
153
|
+
body.tool_choice = 'auto';
|
|
154
|
+
body.parallel_tool_calls = true;
|
|
155
|
+
for (const item of input) {
|
|
156
|
+
if ((item.role === 'user' || item.role === 'assistant') &&
|
|
157
|
+
item.content !== undefined)
|
|
158
|
+
item.type = 'message';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
152
161
|
if (request.tools?.length) {
|
|
153
162
|
body.tools = request.tools.map((tool) => ({
|
|
154
163
|
type: 'function',
|
|
@@ -601,9 +610,11 @@ export class ResponsesCodec {
|
|
|
601
610
|
maxReasoningBytes;
|
|
602
611
|
label;
|
|
603
612
|
configuredThinking;
|
|
613
|
+
requestDialect;
|
|
604
614
|
constructor(options = {}) {
|
|
605
615
|
this.label = options.providerLabel ?? 'Responses provider';
|
|
606
616
|
this.configuredThinking = options.thinking;
|
|
617
|
+
this.requestDialect = options.requestDialect ?? 'public';
|
|
607
618
|
if (options.thinking !== undefined &&
|
|
608
619
|
options.thinking.mode !== 'disabled' &&
|
|
609
620
|
options.thinking.mode !== 'enabled' &&
|
|
@@ -619,7 +630,7 @@ export class ResponsesCodec {
|
|
|
619
630
|
}
|
|
620
631
|
serialize(request, model) {
|
|
621
632
|
try {
|
|
622
|
-
return serializeResponsesRequest(request, model, this.configuredThinking);
|
|
633
|
+
return serializeResponsesRequest(request, model, this.configuredThinking, this.requestDialect);
|
|
623
634
|
}
|
|
624
635
|
catch (error) {
|
|
625
636
|
throw this.contextualize(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "praxis-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Local-first, single-user general agent for the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wuqisen",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
|
|
64
64
|
"test:fixtures": "node scripts/run-fixture-contracts.mjs",
|
|
65
65
|
"test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts",
|
|
66
|
-
"test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts",
|
|
66
|
+
"test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts src/evals/held-out-corpus-v3.test.ts",
|
|
67
67
|
"test:eval:held-out-qualification": "vitest run src/evals/held-out-qualification.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"engines": {
|