@navels/neal 0.1.0 → 0.2.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/dist/neal/adjudicator/planning.js +48 -0
- package/dist/neal/agents/prompts.js +3 -0
- package/dist/neal/agents/rounds.js +8 -0
- package/dist/neal/agents/schemas.js +575 -496
- package/dist/neal/agents/structured-json.js +36 -0
- package/dist/neal/config.js +24 -0
- package/dist/neal/git.js +9 -3
- package/dist/neal/orchestrator/completion.js +167 -112
- package/dist/neal/orchestrator/phases/planning.js +14 -39
- package/dist/neal/orchestrator/split-plan.js +12 -11
- package/dist/neal/orchestrator/transitions.js +30 -71
- package/dist/neal/plan-doc.js +24 -1
- package/dist/neal/prompts/assert-builder.js +8 -1
- package/dist/neal/prompts/execute.js +4 -0
- package/dist/neal/prompts/specialized.js +22 -6
- package/dist/neal/prompts/specs.js +58 -0
- package/dist/neal/providers/anthropic-claude.js +291 -247
- package/dist/neal/providers/generic-agentic.js +18 -0
- package/dist/neal/providers/openai-codex.js +77 -201
- package/dist/neal/providers/openai-compatible.js +33 -5
- package/dist/neal/providers/pricing.js +124 -0
- package/dist/neal/providers/rate-card.js +2301 -0
- package/dist/neal/providers/telemetry.js +4 -0
- package/dist/neal/retrospective.js +33 -4
- package/dist/neal/run-metrics.js +74 -9
- package/docs/compatible-models.md +11 -0
- package/docs/issue-pipeline.md +124 -0
- package/docs/maintenance.md +30 -19
- package/docs/providers.md +117 -0
- package/docs/release.md +29 -25
- package/package.json +7 -3
|
@@ -44,6 +44,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
44
44
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
45
45
|
import { generateText, jsonSchema, NoObjectGeneratedError, Output, stepCountIs, } from 'ai';
|
|
46
46
|
import { getOpenAICompatibleSettings } from '../config.js';
|
|
47
|
+
import { resolveRateCost } from './pricing.js';
|
|
47
48
|
import { createCoderToolset, createPlanAuthorToolset, createReadOnlyToolset, } from './generic-agentic-tools.js';
|
|
48
49
|
import { NealProviderError } from './types.js';
|
|
49
50
|
const GENERIC_AGENTIC_PROVIDER_ID = 'generic-agentic';
|
|
@@ -404,6 +405,7 @@ function resolveGenericAgenticSettings(args) {
|
|
|
404
405
|
apiKey: settings.apiKey,
|
|
405
406
|
model,
|
|
406
407
|
headers: settings.headers,
|
|
408
|
+
pricing: settings.pricing,
|
|
407
409
|
};
|
|
408
410
|
}
|
|
409
411
|
const CODER_STEP_CAP = {
|
|
@@ -617,16 +619,28 @@ async function runAgentModelTurn(ctx, turnOptions) {
|
|
|
617
619
|
toolErrors: { ...ctx.state.toolErrors },
|
|
618
620
|
...(ctx.includeStepsTelemetry ? { steps: ctx.state.steps } : {}),
|
|
619
621
|
};
|
|
622
|
+
// Rate-computed cost: config pricing (when present) wins; otherwise a
|
|
623
|
+
// card-listed model is priced by its resolved slug with no configuration.
|
|
624
|
+
// When neither yields pricing the cost fields are omitted so the
|
|
625
|
+
// tokens-only event shape is preserved (resolveRateCost returns null,
|
|
626
|
+
// matching the prior `computeRateCostUsd`-returns-0-for-empty behavior).
|
|
627
|
+
const cost = resolveRateCost({
|
|
628
|
+
usage: result.usage,
|
|
629
|
+
model: ctx.state.modelSlug,
|
|
630
|
+
configPricing: ctx.state.pricing,
|
|
631
|
+
}) ?? {};
|
|
620
632
|
await emitProviderEvent(ctx.events, {
|
|
621
633
|
type: 'usage_reported',
|
|
622
634
|
...base,
|
|
623
635
|
usage: result.usage,
|
|
636
|
+
...cost,
|
|
624
637
|
providerData,
|
|
625
638
|
});
|
|
626
639
|
await emitProviderEvent(ctx.events, {
|
|
627
640
|
type: 'turn_completed',
|
|
628
641
|
...base,
|
|
629
642
|
usage: result.usage,
|
|
643
|
+
...cost,
|
|
630
644
|
providerData,
|
|
631
645
|
});
|
|
632
646
|
return {
|
|
@@ -974,6 +988,7 @@ class GenericAgenticCoderAdapter {
|
|
|
974
988
|
});
|
|
975
989
|
const state = {
|
|
976
990
|
model,
|
|
991
|
+
modelSlug: settings.model,
|
|
977
992
|
// Assigned immediately below; the toolset's event hook needs the state
|
|
978
993
|
// object to update the cumulative per-tool telemetry maps.
|
|
979
994
|
tools: undefined,
|
|
@@ -981,6 +996,7 @@ class GenericAgenticCoderAdapter {
|
|
|
981
996
|
toolCalls: {},
|
|
982
997
|
toolErrors: {},
|
|
983
998
|
steps: 0,
|
|
999
|
+
pricing: settings.pricing,
|
|
984
1000
|
};
|
|
985
1001
|
const createToolset = args.toolPolicy?.allowRun === false ? createPlanAuthorToolset : createCoderToolset;
|
|
986
1002
|
state.tools = createToolset(args.cwd, {
|
|
@@ -1105,6 +1121,7 @@ class GenericAgenticStructuredAdvisorAdapter {
|
|
|
1105
1121
|
});
|
|
1106
1122
|
const state = {
|
|
1107
1123
|
model,
|
|
1124
|
+
modelSlug: settings.model,
|
|
1108
1125
|
// Assigned immediately below; the toolset's event hook needs the
|
|
1109
1126
|
// state object to update the cumulative per-tool telemetry maps.
|
|
1110
1127
|
tools: undefined,
|
|
@@ -1112,6 +1129,7 @@ class GenericAgenticStructuredAdvisorAdapter {
|
|
|
1112
1129
|
toolCalls: {},
|
|
1113
1130
|
toolErrors: {},
|
|
1114
1131
|
steps: 0,
|
|
1132
|
+
pricing: settings.pricing,
|
|
1115
1133
|
};
|
|
1116
1134
|
// Read-only inspection toolset only: advisor rounds must never gain
|
|
1117
1135
|
// write or shell access. Tool events forward with the advisor role and
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Codex } from '@openai/codex-sdk';
|
|
2
2
|
import { runStructuredJsonProtocol } from '../agents/structured-json.js';
|
|
3
|
+
import { resolveRateCost } from './pricing.js';
|
|
3
4
|
import { NealProviderError } from './types.js';
|
|
4
5
|
const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
5
6
|
class CodexInactivityTimeoutError extends Error {
|
|
@@ -269,7 +270,17 @@ async function emitCodexStreamError(args) {
|
|
|
269
270
|
});
|
|
270
271
|
return true;
|
|
271
272
|
}
|
|
272
|
-
|
|
273
|
+
// Shared turn consumer behind `consumeCodexTurn` (coder) and
|
|
274
|
+
// `consumeCodexAdvisorTurn` (structured advisor). The two paths differ only
|
|
275
|
+
// in the role stamped on emitted events and normalized errors, the
|
|
276
|
+
// advisor-only `label` stamp, and the coder-only `onSessionStarted`
|
|
277
|
+
// callback; everything else is identical.
|
|
278
|
+
async function consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, options) {
|
|
279
|
+
const { role, label, events, onSessionStarted, abortController, model } = options;
|
|
280
|
+
// The advisor path stamps `label` on every emitted event while the coder
|
|
281
|
+
// path emits events without a `label` property at all — the conditional
|
|
282
|
+
// spread preserves that own-property distinction exactly.
|
|
283
|
+
const labelFields = label === undefined ? {} : { label };
|
|
273
284
|
let finalResponse = '';
|
|
274
285
|
let observedFailure = null;
|
|
275
286
|
let sessionHandle = null;
|
|
@@ -282,13 +293,13 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
282
293
|
catch (error) {
|
|
283
294
|
if (observedFailure) {
|
|
284
295
|
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
285
|
-
role
|
|
296
|
+
role,
|
|
286
297
|
sessionHandle,
|
|
287
298
|
cause: error,
|
|
288
299
|
});
|
|
289
300
|
}
|
|
290
301
|
throw normalizeCodexProviderError(error, {
|
|
291
|
-
role
|
|
302
|
+
role,
|
|
292
303
|
sessionHandle,
|
|
293
304
|
});
|
|
294
305
|
}
|
|
@@ -302,7 +313,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
302
313
|
await emitProviderEvent(events, {
|
|
303
314
|
type: 'session_started',
|
|
304
315
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
305
|
-
role
|
|
316
|
+
role,
|
|
317
|
+
...labelFields,
|
|
306
318
|
sessionHandle: event.thread_id,
|
|
307
319
|
providerData: { sdkEventType: event.type },
|
|
308
320
|
});
|
|
@@ -312,7 +324,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
312
324
|
await emitProviderEvent(events, {
|
|
313
325
|
type: 'turn_started',
|
|
314
326
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
315
|
-
role
|
|
327
|
+
role,
|
|
328
|
+
...labelFields,
|
|
316
329
|
sessionHandle,
|
|
317
330
|
providerData: { sdkEventType: event.type },
|
|
318
331
|
});
|
|
@@ -321,7 +334,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
321
334
|
await emitProviderEvent(events, {
|
|
322
335
|
type: 'tool_started',
|
|
323
336
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
324
|
-
role
|
|
337
|
+
role,
|
|
338
|
+
...labelFields,
|
|
325
339
|
sessionHandle,
|
|
326
340
|
itemId: summarizeItem(event.item).itemId,
|
|
327
341
|
toolName: event.item.type,
|
|
@@ -332,7 +346,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
332
346
|
await emitProviderEvent(events, {
|
|
333
347
|
type: 'tool_progress',
|
|
334
348
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
335
|
-
role
|
|
349
|
+
role,
|
|
350
|
+
...labelFields,
|
|
336
351
|
sessionHandle,
|
|
337
352
|
itemId: summarizeItem(event.item).itemId,
|
|
338
353
|
toolName: event.item.type,
|
|
@@ -344,7 +359,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
344
359
|
await emitProviderEvent(events, {
|
|
345
360
|
type: 'command_completed',
|
|
346
361
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
347
|
-
role
|
|
362
|
+
role,
|
|
363
|
+
...labelFields,
|
|
348
364
|
sessionHandle,
|
|
349
365
|
itemId: event.item.id,
|
|
350
366
|
command: event.item.command,
|
|
@@ -361,7 +377,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
361
377
|
await emitProviderEvent(events, {
|
|
362
378
|
type: 'file_changed',
|
|
363
379
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
364
|
-
role
|
|
380
|
+
role,
|
|
381
|
+
...labelFields,
|
|
365
382
|
sessionHandle,
|
|
366
383
|
files,
|
|
367
384
|
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
@@ -371,206 +388,41 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
371
388
|
finalResponse = event.item.text;
|
|
372
389
|
}
|
|
373
390
|
break;
|
|
374
|
-
case 'turn.completed':
|
|
391
|
+
case 'turn.completed': {
|
|
392
|
+
// Card-derived cost only when the turn actually carried usage. Codex
|
|
393
|
+
// advertises opportunistic usage reporting, and `computeRateCostUsd`
|
|
394
|
+
// returns 0 for absent usage, so gating on `event.usage !== undefined`
|
|
395
|
+
// (matching the usage_reported emit) keeps a usage-less turn's
|
|
396
|
+
// tokens-only shape byte-stable rather than attaching costUsd: 0. Codex
|
|
397
|
+
// has no config pricing, so cost comes from the card via the configured
|
|
398
|
+
// model only.
|
|
399
|
+
const cost = event.usage !== undefined
|
|
400
|
+
? (resolveRateCost({ usage: event.usage, model: model ?? null, configPricing: null }) ?? {})
|
|
401
|
+
: {};
|
|
375
402
|
await emitProviderEvent(events, {
|
|
376
403
|
type: 'turn_completed',
|
|
377
404
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
378
|
-
role
|
|
405
|
+
role,
|
|
406
|
+
...labelFields,
|
|
379
407
|
sessionHandle,
|
|
380
408
|
usage: event.usage,
|
|
409
|
+
...cost,
|
|
381
410
|
providerData: { sdkEventType: event.type },
|
|
382
411
|
});
|
|
383
412
|
if (event.usage !== undefined) {
|
|
384
413
|
await emitProviderEvent(events, {
|
|
385
414
|
type: 'usage_reported',
|
|
386
415
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
387
|
-
role
|
|
416
|
+
role,
|
|
417
|
+
...labelFields,
|
|
388
418
|
sessionHandle,
|
|
389
419
|
usage: event.usage,
|
|
420
|
+
...cost,
|
|
390
421
|
providerData: { sdkEventType: event.type },
|
|
391
422
|
});
|
|
392
423
|
}
|
|
393
424
|
break;
|
|
394
|
-
case 'turn.failed':
|
|
395
|
-
{
|
|
396
|
-
const nextFailure = observeCodexFailure({
|
|
397
|
-
message: event.error.message,
|
|
398
|
-
source: event.type,
|
|
399
|
-
cause: event.error,
|
|
400
|
-
});
|
|
401
|
-
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
402
|
-
observedFailure = nextFailure;
|
|
403
|
-
await emitProviderEvent(events, {
|
|
404
|
-
type: 'provider_error',
|
|
405
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
406
|
-
role: 'coder',
|
|
407
|
-
sessionHandle,
|
|
408
|
-
message: event.error.message,
|
|
409
|
-
providerData: { sdkEventType: event.type },
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
break;
|
|
414
|
-
case 'error':
|
|
415
|
-
{
|
|
416
|
-
const nextFailure = observeCodexFailure({
|
|
417
|
-
message: event.message,
|
|
418
|
-
source: event.type,
|
|
419
|
-
cause: event,
|
|
420
|
-
});
|
|
421
|
-
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
422
|
-
if (await emitCodexStreamError({
|
|
423
|
-
events,
|
|
424
|
-
role: 'coder',
|
|
425
|
-
sessionHandle,
|
|
426
|
-
message: event.message,
|
|
427
|
-
})) {
|
|
428
|
-
observedFailure = nextFailure;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
break;
|
|
433
|
-
default:
|
|
434
|
-
break;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
if (observedFailure) {
|
|
438
|
-
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
439
|
-
role: 'coder',
|
|
440
|
-
sessionHandle,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
return { finalResponse, sessionHandle };
|
|
444
|
-
}
|
|
445
|
-
async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, events, abortController) {
|
|
446
|
-
let finalResponse = '';
|
|
447
|
-
let observedFailure = null;
|
|
448
|
-
let sessionHandle = null;
|
|
449
|
-
const iterator = turn.events[Symbol.asyncIterator]();
|
|
450
|
-
while (true) {
|
|
451
|
-
let next;
|
|
452
|
-
try {
|
|
453
|
-
next = await nextWithTimeout(iterator.next(), inactivityTimeoutMs, () => abortController?.abort());
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
if (observedFailure) {
|
|
457
|
-
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
458
|
-
role: 'structured-advisor',
|
|
459
|
-
sessionHandle,
|
|
460
|
-
cause: error,
|
|
461
|
-
});
|
|
462
425
|
}
|
|
463
|
-
throw normalizeCodexProviderError(error, {
|
|
464
|
-
role: 'structured-advisor',
|
|
465
|
-
sessionHandle,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (next.done) {
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
const event = next.value;
|
|
472
|
-
switch (event.type) {
|
|
473
|
-
case 'thread.started':
|
|
474
|
-
sessionHandle = event.thread_id;
|
|
475
|
-
await emitProviderEvent(events, {
|
|
476
|
-
type: 'session_started',
|
|
477
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
478
|
-
role: 'structured-advisor',
|
|
479
|
-
label,
|
|
480
|
-
sessionHandle: event.thread_id,
|
|
481
|
-
providerData: { sdkEventType: event.type },
|
|
482
|
-
});
|
|
483
|
-
break;
|
|
484
|
-
case 'turn.started':
|
|
485
|
-
await emitProviderEvent(events, {
|
|
486
|
-
type: 'turn_started',
|
|
487
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
488
|
-
role: 'structured-advisor',
|
|
489
|
-
label,
|
|
490
|
-
sessionHandle,
|
|
491
|
-
providerData: { sdkEventType: event.type },
|
|
492
|
-
});
|
|
493
|
-
break;
|
|
494
|
-
case 'item.started':
|
|
495
|
-
await emitProviderEvent(events, {
|
|
496
|
-
type: 'tool_started',
|
|
497
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
498
|
-
role: 'structured-advisor',
|
|
499
|
-
label,
|
|
500
|
-
sessionHandle,
|
|
501
|
-
itemId: summarizeItem(event.item).itemId,
|
|
502
|
-
toolName: event.item.type,
|
|
503
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
504
|
-
});
|
|
505
|
-
break;
|
|
506
|
-
case 'item.updated':
|
|
507
|
-
await emitProviderEvent(events, {
|
|
508
|
-
type: 'tool_progress',
|
|
509
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
510
|
-
role: 'structured-advisor',
|
|
511
|
-
label,
|
|
512
|
-
sessionHandle,
|
|
513
|
-
itemId: summarizeItem(event.item).itemId,
|
|
514
|
-
toolName: event.item.type,
|
|
515
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
516
|
-
});
|
|
517
|
-
break;
|
|
518
|
-
case 'item.completed':
|
|
519
|
-
if (event.item.type === 'command_execution') {
|
|
520
|
-
await emitProviderEvent(events, {
|
|
521
|
-
type: 'command_completed',
|
|
522
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
523
|
-
role: 'structured-advisor',
|
|
524
|
-
label,
|
|
525
|
-
sessionHandle,
|
|
526
|
-
itemId: event.item.id,
|
|
527
|
-
command: event.item.command,
|
|
528
|
-
status: event.item.status,
|
|
529
|
-
exitCode: event.item.exit_code ?? null,
|
|
530
|
-
output: event.item.aggregated_output,
|
|
531
|
-
outputLength: event.item.aggregated_output.length,
|
|
532
|
-
cwd,
|
|
533
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
else if (event.item.type === 'file_change' && event.item.changes.length > 0) {
|
|
537
|
-
const files = event.item.changes.map((change) => change.path);
|
|
538
|
-
await emitProviderEvent(events, {
|
|
539
|
-
type: 'file_changed',
|
|
540
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
541
|
-
role: 'structured-advisor',
|
|
542
|
-
label,
|
|
543
|
-
sessionHandle,
|
|
544
|
-
files,
|
|
545
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
else if (event.item.type === 'agent_message') {
|
|
549
|
-
finalResponse = event.item.text;
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
case 'turn.completed':
|
|
553
|
-
await emitProviderEvent(events, {
|
|
554
|
-
type: 'turn_completed',
|
|
555
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
556
|
-
role: 'structured-advisor',
|
|
557
|
-
label,
|
|
558
|
-
sessionHandle,
|
|
559
|
-
usage: event.usage,
|
|
560
|
-
providerData: { sdkEventType: event.type },
|
|
561
|
-
});
|
|
562
|
-
if (event.usage !== undefined) {
|
|
563
|
-
await emitProviderEvent(events, {
|
|
564
|
-
type: 'usage_reported',
|
|
565
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
566
|
-
role: 'structured-advisor',
|
|
567
|
-
label,
|
|
568
|
-
sessionHandle,
|
|
569
|
-
usage: event.usage,
|
|
570
|
-
providerData: { sdkEventType: event.type },
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
break;
|
|
574
426
|
case 'turn.failed':
|
|
575
427
|
{
|
|
576
428
|
const nextFailure = observeCodexFailure({
|
|
@@ -583,8 +435,8 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
583
435
|
await emitProviderEvent(events, {
|
|
584
436
|
type: 'provider_error',
|
|
585
437
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
586
|
-
role
|
|
587
|
-
|
|
438
|
+
role,
|
|
439
|
+
...labelFields,
|
|
588
440
|
sessionHandle,
|
|
589
441
|
message: event.error.message,
|
|
590
442
|
providerData: { sdkEventType: event.type },
|
|
@@ -602,7 +454,7 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
602
454
|
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
603
455
|
if (await emitCodexStreamError({
|
|
604
456
|
events,
|
|
605
|
-
role
|
|
457
|
+
role,
|
|
606
458
|
label,
|
|
607
459
|
sessionHandle,
|
|
608
460
|
message: event.message,
|
|
@@ -618,12 +470,36 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
618
470
|
}
|
|
619
471
|
if (observedFailure) {
|
|
620
472
|
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
621
|
-
role
|
|
473
|
+
role,
|
|
622
474
|
sessionHandle,
|
|
623
475
|
});
|
|
624
476
|
}
|
|
625
477
|
return { finalResponse, sessionHandle };
|
|
626
478
|
}
|
|
479
|
+
async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessionStarted, abortController,
|
|
480
|
+
// Appended last so existing positional callers (including test hooks) stay
|
|
481
|
+
// valid; forwards the configured slug for card-derived cost.
|
|
482
|
+
model) {
|
|
483
|
+
return consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, {
|
|
484
|
+
role: 'coder',
|
|
485
|
+
events,
|
|
486
|
+
onSessionStarted,
|
|
487
|
+
abortController,
|
|
488
|
+
model,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, events, abortController,
|
|
492
|
+
// Appended last so existing positional callers (including test hooks) stay
|
|
493
|
+
// valid; forwards the configured slug for card-derived cost.
|
|
494
|
+
model) {
|
|
495
|
+
return consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, {
|
|
496
|
+
role: 'structured-advisor',
|
|
497
|
+
label,
|
|
498
|
+
events,
|
|
499
|
+
abortController,
|
|
500
|
+
model,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
627
503
|
function parseCodexStructuredOutput(finalResponse, label, sessionHandle) {
|
|
628
504
|
try {
|
|
629
505
|
return JSON.parse(finalResponse);
|
|
@@ -685,7 +561,7 @@ class OpenAICodexCoderAdapter {
|
|
|
685
561
|
...(args.outputSchema ? { outputSchema: args.outputSchema } : {}),
|
|
686
562
|
signal: abortController.signal,
|
|
687
563
|
});
|
|
688
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
564
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController, this.options.model ?? null);
|
|
689
565
|
if (args.outputSchema) {
|
|
690
566
|
await emitProviderEvent(args.events, {
|
|
691
567
|
type: 'structured_output_received',
|
|
@@ -745,7 +621,7 @@ class OpenAICodexCoderAdapter {
|
|
|
745
621
|
const streamedTurn = await thread.runStreamed(prompt, {
|
|
746
622
|
signal: abortController.signal,
|
|
747
623
|
});
|
|
748
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
624
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController, this.options.model ?? null);
|
|
749
625
|
return {
|
|
750
626
|
assistantText: result.finalResponse,
|
|
751
627
|
sessionHandle: thread.id,
|
|
@@ -767,7 +643,7 @@ class OpenAICodexCoderAdapter {
|
|
|
767
643
|
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
768
644
|
signal: repairAbortController.signal,
|
|
769
645
|
});
|
|
770
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, undefined, repairAbortController);
|
|
646
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, undefined, repairAbortController, this.options.model ?? null);
|
|
771
647
|
return {
|
|
772
648
|
assistantText: result.finalResponse,
|
|
773
649
|
sessionHandle: repairThread.id,
|
|
@@ -865,7 +741,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
865
741
|
const streamedTurn = await thread.runStreamed(prompt, {
|
|
866
742
|
signal: abortController.signal,
|
|
867
743
|
});
|
|
868
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
744
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController, this.options.model ?? null);
|
|
869
745
|
return {
|
|
870
746
|
assistantText: result.finalResponse,
|
|
871
747
|
sessionHandle: thread.id ?? result.sessionHandle ?? null,
|
|
@@ -883,7 +759,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
883
759
|
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
884
760
|
signal: repairAbortController.signal,
|
|
885
761
|
});
|
|
886
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, repairAbortController);
|
|
762
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, repairAbortController, this.options.model ?? null);
|
|
887
763
|
return {
|
|
888
764
|
assistantText: result.finalResponse,
|
|
889
765
|
sessionHandle: repairThread.id ?? result.sessionHandle ?? null,
|
|
@@ -900,7 +776,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
900
776
|
});
|
|
901
777
|
}
|
|
902
778
|
const streamedTurn = await thread.runStreamed(args.prompt, buildCodexStructuredAdvisorRunOptions(args, abortController.signal));
|
|
903
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
779
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController, this.options.model ?? null);
|
|
904
780
|
const structured = parseCodexStructuredOutput(result.finalResponse, args.label, thread.id);
|
|
905
781
|
await emitProviderEvent(args.events, {
|
|
906
782
|
type: 'structured_output_received',
|
|
@@ -31,6 +31,7 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
|
31
31
|
import { generateText } from 'ai';
|
|
32
32
|
import { runStructuredJsonProtocol } from '../agents/structured-json.js';
|
|
33
33
|
import { getOpenAICompatibleSettings } from '../config.js';
|
|
34
|
+
import { resolveRateCost } from './pricing.js';
|
|
34
35
|
import { NealProviderError } from './types.js';
|
|
35
36
|
const OPENAI_COMPATIBLE_PROVIDER_ID = 'openai-compatible';
|
|
36
37
|
const API_RETRY_BASE_DELAY_MS = 500;
|
|
@@ -123,7 +124,11 @@ function describeError(error) {
|
|
|
123
124
|
* the cancellation; never retried in-round);
|
|
124
125
|
* - abort without a caller abort -> inactivity expiry -> `timeout`,
|
|
125
126
|
* retryable: true;
|
|
126
|
-
* - HTTP 401
|
|
127
|
+
* - HTTP 401 -> `permission_denied`, retryable (OpenAI intermittently
|
|
128
|
+
* returns 401 for keys that are valid moments before and after; a
|
|
129
|
+
* persistent 401 exhausts the bounded retry budget and still surfaces
|
|
130
|
+
* as `permission_denied`);
|
|
131
|
+
* - HTTP 403 -> `permission_denied`, non-retryable;
|
|
127
132
|
* - HTTP 408/429/5xx -> `api_error`, retryable;
|
|
128
133
|
* - any other HTTP status -> `provider_failed`, non-retryable (Neal's
|
|
129
134
|
* status table is authoritative for status-bearing errors; the SDK's
|
|
@@ -165,7 +170,10 @@ function normalizeOpenAICompatibleError(error, ctx) {
|
|
|
165
170
|
message: `OpenAI-compatible ${ctx.label} request was rejected with HTTP ${status}: ${describeError(error)}`,
|
|
166
171
|
sessionHandle: ctx.sessionHandle,
|
|
167
172
|
kind: 'permission_denied',
|
|
168
|
-
|
|
173
|
+
// 401 gets the bounded api-retry budget: OpenAI intermittently
|
|
174
|
+
// rejects otherwise-valid keys with 401, and a single blip must
|
|
175
|
+
// not terminate an unattended run. 403 stays terminal.
|
|
176
|
+
retryable: status === 401,
|
|
169
177
|
cause: error,
|
|
170
178
|
});
|
|
171
179
|
}
|
|
@@ -237,7 +245,7 @@ function createEmbeddedResponseError(args) {
|
|
|
237
245
|
message,
|
|
238
246
|
sessionHandle: args.sessionHandle,
|
|
239
247
|
kind: 'permission_denied',
|
|
240
|
-
retryable:
|
|
248
|
+
retryable: status === 401,
|
|
241
249
|
});
|
|
242
250
|
}
|
|
243
251
|
if (status === 408 || status === 429 || (status !== null && status >= 500)) {
|
|
@@ -415,6 +423,7 @@ function resolveRoundSettings(args) {
|
|
|
415
423
|
apiKey: settings.apiKey,
|
|
416
424
|
model,
|
|
417
425
|
headers: settings.headers,
|
|
426
|
+
pricing: settings.pricing,
|
|
418
427
|
};
|
|
419
428
|
}
|
|
420
429
|
class OpenAICompatibleStructuredAdvisorAdapter {
|
|
@@ -469,9 +478,11 @@ class OpenAICompatibleStructuredAdvisorAdapter {
|
|
|
469
478
|
runInitial: async (prompt) => ({
|
|
470
479
|
assistantText: await this.runChatTurnWithRetry({
|
|
471
480
|
model,
|
|
481
|
+
modelSlug: settings.model,
|
|
472
482
|
prompt,
|
|
473
483
|
roundArgs: args,
|
|
474
484
|
sessionHandle,
|
|
485
|
+
pricing: settings.pricing,
|
|
475
486
|
// Only the primary turn is wired to external cancellation; repair
|
|
476
487
|
// turns are short prompt-only turns (matching other adapters).
|
|
477
488
|
signal: args.signal,
|
|
@@ -481,9 +492,11 @@ class OpenAICompatibleStructuredAdvisorAdapter {
|
|
|
481
492
|
runRepair: async (prompt) => ({
|
|
482
493
|
assistantText: await this.runChatTurnWithRetry({
|
|
483
494
|
model,
|
|
495
|
+
modelSlug: settings.model,
|
|
484
496
|
prompt,
|
|
485
497
|
roundArgs: args,
|
|
486
498
|
sessionHandle,
|
|
499
|
+
pricing: settings.pricing,
|
|
487
500
|
}),
|
|
488
501
|
sessionHandle,
|
|
489
502
|
}),
|
|
@@ -522,7 +535,10 @@ class OpenAICompatibleStructuredAdvisorAdapter {
|
|
|
522
535
|
/**
|
|
523
536
|
* One chat turn with the bounded transient-retry loop: emits
|
|
524
537
|
* `turn_started`, makes a single tool-less `generateText` call
|
|
525
|
-
* (`
|
|
538
|
+
* (`maxRetries: 0`, abort/inactivity signal; no sampling parameters —
|
|
539
|
+
* reasoning-family models such as OpenAI's o-series and gpt-5.x reject any
|
|
540
|
+
* non-default `temperature` with HTTP 400, and the structured-output
|
|
541
|
+
* protocol, not sampling, is what disciplines the response), applies
|
|
526
542
|
* the reasoning-text fallback, then emits `assistant_text`,
|
|
527
543
|
* `turn_completed`, and `usage_reported` (the latter only when the
|
|
528
544
|
* response body actually carried usage, observed structurally via the AI
|
|
@@ -549,7 +565,6 @@ class OpenAICompatibleStructuredAdvisorAdapter {
|
|
|
549
565
|
const result = await generateText({
|
|
550
566
|
model: callArgs.model,
|
|
551
567
|
prompt: callArgs.prompt,
|
|
552
|
-
temperature: 0,
|
|
553
568
|
maxRetries: 0,
|
|
554
569
|
abortSignal: composeAbortSignal(callArgs.signal, roundArgs.inactivityTimeoutMs),
|
|
555
570
|
});
|
|
@@ -577,16 +592,29 @@ class OpenAICompatibleStructuredAdvisorAdapter {
|
|
|
577
592
|
// marker, matching the bespoke adapter's "only report usage when the
|
|
578
593
|
// gateway provided it" behavior).
|
|
579
594
|
const hasUsage = result.usage.raw !== undefined;
|
|
595
|
+
// Rate-computed cost only when the round has usage. Config pricing (when
|
|
596
|
+
// present) wins; otherwise a card-listed model is priced by its resolved
|
|
597
|
+
// slug with no configuration. When neither yields pricing the cost fields
|
|
598
|
+
// are omitted entirely so the tokens-only event shape stays byte-stable.
|
|
599
|
+
const cost = hasUsage
|
|
600
|
+
? (resolveRateCost({
|
|
601
|
+
usage: result.usage,
|
|
602
|
+
model: callArgs.modelSlug,
|
|
603
|
+
configPricing: callArgs.pricing,
|
|
604
|
+
}) ?? {})
|
|
605
|
+
: {};
|
|
580
606
|
await emitProviderEvent(roundArgs.events, {
|
|
581
607
|
type: 'turn_completed',
|
|
582
608
|
...base,
|
|
583
609
|
...(hasUsage ? { usage: result.usage } : {}),
|
|
610
|
+
...cost,
|
|
584
611
|
});
|
|
585
612
|
if (hasUsage) {
|
|
586
613
|
await emitProviderEvent(roundArgs.events, {
|
|
587
614
|
type: 'usage_reported',
|
|
588
615
|
...base,
|
|
589
616
|
usage: result.usage,
|
|
617
|
+
...cost,
|
|
590
618
|
});
|
|
591
619
|
}
|
|
592
620
|
return assistantText;
|