@askalf/dario 5.5.88 → 5.5.90

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.
@@ -516,6 +516,22 @@ export declare function parseResponsesSSEEvent(eventLine: string | undefined, da
516
516
  * Call `flush()` at end-of-stream to parse any trailing record that was
517
517
  * not blank-line terminated. Pure and offline-testable.
518
518
  */
519
+ /**
520
+ * Fold the Anthropic stream this module produces back into ONE Message.
521
+ *
522
+ * Needed because the ChatGPT Codex backend's terminal `response.completed`
523
+ * carries `output: []` — the content only ever exists in the delta events, so
524
+ * `responsesToAnthropicResponse(terminal)` yields an empty message. (The
525
+ * standard Responses API does populate `output`, which is why this was not
526
+ * obvious.) The chat path has always avoided the trap by accumulating from
527
+ * deltas; this is the same discipline for the Anthropic shape, and it means
528
+ * the streaming and non-streaming bodies are assembled from ONE translation
529
+ * rather than two that can disagree. dario#1143.
530
+ */
531
+ export declare function createAnthropicMessageAssembler(): {
532
+ push(events: readonly ResponsesAnthropicStreamEvent[]): void;
533
+ message(fallbackModel: string): AnthropicResponseWithThinking;
534
+ };
519
535
  export declare function createResponsesSSEParser(): {
520
536
  push(chunk: string): ResponsesStreamEvent[];
521
537
  flush(): ResponsesStreamEvent[];
@@ -791,6 +791,75 @@ export function parseResponsesSSEEvent(eventLine, dataLine) {
791
791
  * Call `flush()` at end-of-stream to parse any trailing record that was
792
792
  * not blank-line terminated. Pure and offline-testable.
793
793
  */
794
+ /**
795
+ * Fold the Anthropic stream this module produces back into ONE Message.
796
+ *
797
+ * Needed because the ChatGPT Codex backend's terminal `response.completed`
798
+ * carries `output: []` — the content only ever exists in the delta events, so
799
+ * `responsesToAnthropicResponse(terminal)` yields an empty message. (The
800
+ * standard Responses API does populate `output`, which is why this was not
801
+ * obvious.) The chat path has always avoided the trap by accumulating from
802
+ * deltas; this is the same discipline for the Anthropic shape, and it means
803
+ * the streaming and non-streaming bodies are assembled from ONE translation
804
+ * rather than two that can disagree. dario#1143.
805
+ */
806
+ export function createAnthropicMessageAssembler() {
807
+ let msg = null;
808
+ const blocks = [];
809
+ const partialJson = new Map();
810
+ return {
811
+ push(events) {
812
+ for (const e of events) {
813
+ if (e.type === 'message_start') {
814
+ msg = { ...e.message, content: [] };
815
+ }
816
+ else if (e.type === 'content_block_start') {
817
+ blocks[e.index] = { ...e.content_block };
818
+ if (e.content_block.type === 'tool_use')
819
+ partialJson.set(e.index, '');
820
+ }
821
+ else if (e.type === 'content_block_delta') {
822
+ const b = blocks[e.index] ?? (blocks[e.index] = {});
823
+ const d = e.delta;
824
+ if (d.type === 'text_delta')
825
+ b.text = String(b.text ?? '') + d.text;
826
+ else if (d.type === 'thinking_delta')
827
+ b.thinking = String(b.thinking ?? '') + d.thinking;
828
+ else if (d.type === 'input_json_delta')
829
+ partialJson.set(e.index, (partialJson.get(e.index) ?? '') + d.partial_json);
830
+ }
831
+ else if (e.type === 'message_delta') {
832
+ if (msg) {
833
+ msg.stop_reason = e.delta.stop_reason;
834
+ const u = msg.usage ?? { input_tokens: 0, output_tokens: 0 };
835
+ msg.usage = {
836
+ ...u,
837
+ output_tokens: e.usage.output_tokens,
838
+ input_tokens: e.usage.input_tokens ?? u.input_tokens,
839
+ };
840
+ }
841
+ }
842
+ }
843
+ },
844
+ message(fallbackModel) {
845
+ for (const [i, raw] of partialJson) {
846
+ const b = blocks[i];
847
+ if (!b || b.type !== 'tool_use')
848
+ continue;
849
+ b.input = safeParseArguments(raw);
850
+ }
851
+ const content = blocks.filter(Boolean);
852
+ if (!msg) {
853
+ return {
854
+ id: 'msg_dario', type: 'message', role: 'assistant', model: fallbackModel,
855
+ content, stop_reason: 'end_turn', stop_sequence: null,
856
+ usage: { input_tokens: 0, output_tokens: 0 },
857
+ };
858
+ }
859
+ return { ...msg, content };
860
+ },
861
+ };
862
+ }
794
863
  export function createResponsesSSEParser() {
795
864
  let buffer = '';
796
865
  const boundary = /\r?\n\r?\n/;
@@ -114,6 +114,27 @@ export declare function createResponsesTranslator(model: string): {
114
114
  /** Everything seen so far, as one non-streaming chat.completion body. */
115
115
  complete(): Record<string, unknown>;
116
116
  };
117
+ /**
118
+ * Fields the ChatGPT Codex backend accepts on /responses.
119
+ *
120
+ * It is NOT the public Responses API: it rejects a whole class of sampling and
121
+ * metadata parameters outright, one 400 at a time —
122
+ * 400 {"detail":"Unsupported parameter: <name>"}
123
+ * Probed directly against a live subscription (2026-08-30); rejected were
124
+ * temperature, top_p, max_output_tokens, presence_penalty, frequency_penalty,
125
+ * seed, metadata, top_logprobs, truncation and service_tier.
126
+ *
127
+ * This is an ALLOWLIST rather than a list of the ten known-bad names on
128
+ * purpose. The backend is undocumented and clearly restrictive, so the failure
129
+ * we must not have is "we started sending a new field and every request 400s".
130
+ * Dropping an unknown field degrades one request; sending one breaks all of
131
+ * them. Both request builders stay correct for an API-key Responses endpoint —
132
+ * which does accept these — because the scrub happens HERE, at the transport
133
+ * that knows which backend it is talking to.
134
+ */
135
+ export declare const CODEX_SUPPORTED_FIELDS: readonly string[];
136
+ /** Drop every field this backend does not accept. Pure; exported for tests. */
137
+ export declare function toCodexSupportedBody(body: Record<string, unknown>): Record<string, unknown>;
117
138
  export declare function buildCodexHeaders(creds: CodexAccountCredentials): Record<string, string>;
118
139
  /**
119
140
  * Serve a request from a stored Codex account, in either client wire shape.
@@ -1,4 +1,4 @@
1
- import { anthropicToResponsesRequest, createResponsesSSEParser, formatResponsesAnthropicSSE, responsesStreamToAnthropicSSE, responsesToAnthropicResponse, } from './anthropic-responses-translate.js';
1
+ import { anthropicToResponsesRequest, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
2
2
  export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
3
3
  /** Originator string the codex CLI identifies itself with. */
4
4
  const CODEX_ORIGINATOR = 'codex_cli_rs';
@@ -377,6 +377,37 @@ export function createResponsesTranslator(model) {
377
377
  },
378
378
  };
379
379
  }
380
+ /**
381
+ * Fields the ChatGPT Codex backend accepts on /responses.
382
+ *
383
+ * It is NOT the public Responses API: it rejects a whole class of sampling and
384
+ * metadata parameters outright, one 400 at a time —
385
+ * 400 {"detail":"Unsupported parameter: <name>"}
386
+ * Probed directly against a live subscription (2026-08-30); rejected were
387
+ * temperature, top_p, max_output_tokens, presence_penalty, frequency_penalty,
388
+ * seed, metadata, top_logprobs, truncation and service_tier.
389
+ *
390
+ * This is an ALLOWLIST rather than a list of the ten known-bad names on
391
+ * purpose. The backend is undocumented and clearly restrictive, so the failure
392
+ * we must not have is "we started sending a new field and every request 400s".
393
+ * Dropping an unknown field degrades one request; sending one breaks all of
394
+ * them. Both request builders stay correct for an API-key Responses endpoint —
395
+ * which does accept these — because the scrub happens HERE, at the transport
396
+ * that knows which backend it is talking to.
397
+ */
398
+ export const CODEX_SUPPORTED_FIELDS = [
399
+ 'model', 'input', 'stream', 'store', 'instructions',
400
+ 'tools', 'tool_choice', 'parallel_tool_calls', 'reasoning',
401
+ ];
402
+ /** Drop every field this backend does not accept. Pure; exported for tests. */
403
+ export function toCodexSupportedBody(body) {
404
+ const out = {};
405
+ for (const k of CODEX_SUPPORTED_FIELDS) {
406
+ if (body[k] !== undefined)
407
+ out[k] = body[k];
408
+ }
409
+ return out;
410
+ }
380
411
  export function buildCodexHeaders(creds) {
381
412
  const headers = {
382
413
  'Content-Type': 'application/json',
@@ -434,17 +465,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
434
465
  const upstreamBody = isAnthropic
435
466
  ? { ...anthropicToResponsesRequest(parsed, model), stream: true }
436
467
  : chatCompletionsToResponses(parsed);
437
- // The ChatGPT Codex backend REJECTS an output cap outright:
438
- // 400 {"detail":"Unsupported parameter: max_output_tokens"}
439
- // Both builders set it from the client's max_tokens / max_completion_tokens,
440
- // so BOTH shapes 400 whenever a client asks for one. It stayed hidden because
441
- // every smoke test so far happened to omit max_tokens; it then showed up as a
442
- // 100% failure on the Anthropic path, where the Messages API REQUIRES
443
- // max_tokens and so always produced it. Stripped here rather than in either
444
- // translator because it is a property of THIS backend, not of either wire
445
- // format — the same builders are correct against an API-key Responses
446
- // endpoint, which does support the parameter.
447
- delete upstreamBody.max_output_tokens;
468
+ const scrubbed = toCodexSupportedBody(upstreamBody);
448
469
  const target = `${CODEX_BACKEND_BASE_URL.replace(/\/$/, '')}/responses`;
449
470
  const abort = new AbortController();
450
471
  const timeout = setTimeout(() => abort.abort(), upstreamTimeoutMs);
@@ -454,7 +475,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
454
475
  const upstream = await fetchImpl(target, {
455
476
  method: 'POST',
456
477
  headers: buildCodexHeaders(creds),
457
- body: JSON.stringify(upstreamBody),
478
+ body: JSON.stringify(scrubbed),
458
479
  signal: abort.signal,
459
480
  });
460
481
  if (!upstream.ok) {
@@ -471,6 +492,10 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
471
492
  const translator = isAnthropic ? null : createResponsesTranslator(model);
472
493
  const sseParser = isAnthropic ? createResponsesSSEParser() : null;
473
494
  const antTranslator = isAnthropic ? responsesStreamToAnthropicSSE({ requestModel: model }) : null;
495
+ // The non-streaming body is FOLDED FROM THE STREAM, not read off the
496
+ // terminal event: this backend sends response.completed with output: [],
497
+ // so the content exists only in the deltas (dario#1143).
498
+ const antAssembler = isAnthropic ? createAnthropicMessageAssembler() : null;
474
499
  let terminalResponse = null;
475
500
  let anthropicFailed = false;
476
501
  const emitAnthropic = (events) => {
@@ -483,7 +508,10 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
483
508
  if (t === 'response.failed')
484
509
  anthropicFailed = true;
485
510
  }
486
- for (const out of antTranslator.push(ev)) {
511
+ const produced = antTranslator.push(ev);
512
+ if (!clientWantsStream)
513
+ antAssembler.push(produced);
514
+ for (const out of produced) {
487
515
  if (clientWantsStream)
488
516
  res.write(formatResponsesAnthropicSSE(out));
489
517
  }
@@ -563,7 +591,8 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
563
591
  'Access-Control-Allow-Origin': corsOrigin,
564
592
  ...securityHeaders,
565
593
  });
566
- res.end(JSON.stringify(responsesToAnthropicResponse(terminalResponse ?? {}, model)));
594
+ antAssembler.push(antTranslator.end());
595
+ res.end(JSON.stringify(antAssembler.message(model)));
567
596
  }
568
597
  }
569
598
  else if (clientWantsStream) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.88",
3
+ "version": "5.5.90",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {