@askalf/dario 5.5.87 → 5.5.89

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/;
@@ -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';
@@ -434,6 +434,17 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
434
434
  const upstreamBody = isAnthropic
435
435
  ? { ...anthropicToResponsesRequest(parsed, model), stream: true }
436
436
  : 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;
437
448
  const target = `${CODEX_BACKEND_BASE_URL.replace(/\/$/, '')}/responses`;
438
449
  const abort = new AbortController();
439
450
  const timeout = setTimeout(() => abort.abort(), upstreamTimeoutMs);
@@ -460,6 +471,10 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
460
471
  const translator = isAnthropic ? null : createResponsesTranslator(model);
461
472
  const sseParser = isAnthropic ? createResponsesSSEParser() : null;
462
473
  const antTranslator = isAnthropic ? responsesStreamToAnthropicSSE({ requestModel: model }) : null;
474
+ // The non-streaming body is FOLDED FROM THE STREAM, not read off the
475
+ // terminal event: this backend sends response.completed with output: [],
476
+ // so the content exists only in the deltas (dario#1143).
477
+ const antAssembler = isAnthropic ? createAnthropicMessageAssembler() : null;
463
478
  let terminalResponse = null;
464
479
  let anthropicFailed = false;
465
480
  const emitAnthropic = (events) => {
@@ -472,7 +487,10 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
472
487
  if (t === 'response.failed')
473
488
  anthropicFailed = true;
474
489
  }
475
- for (const out of antTranslator.push(ev)) {
490
+ const produced = antTranslator.push(ev);
491
+ if (!clientWantsStream)
492
+ antAssembler.push(produced);
493
+ for (const out of produced) {
476
494
  if (clientWantsStream)
477
495
  res.write(formatResponsesAnthropicSSE(out));
478
496
  }
@@ -552,7 +570,8 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
552
570
  'Access-Control-Allow-Origin': corsOrigin,
553
571
  ...securityHeaders,
554
572
  });
555
- res.end(JSON.stringify(responsesToAnthropicResponse(terminalResponse ?? {}, model)));
573
+ antAssembler.push(antTranslator.end());
574
+ res.end(JSON.stringify(antAssembler.message(model)));
556
575
  }
557
576
  }
558
577
  else if (clientWantsStream) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.87",
3
+ "version": "5.5.89",
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": {