@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.
@@ -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
- async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessionStarted, abortController) {
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: 'coder',
296
+ role,
286
297
  sessionHandle,
287
298
  cause: error,
288
299
  });
289
300
  }
290
301
  throw normalizeCodexProviderError(error, {
291
- role: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'coder',
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: 'structured-advisor',
587
- label,
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: 'structured-advisor',
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: 'structured-advisor',
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/403 -> `permission_denied`, non-retryable;
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
- retryable: false,
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: false,
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
- * (`temperature: 0`, `maxRetries: 0`, abort/inactivity signal), applies
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;