@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/;
|
package/dist/codex-backend.d.ts
CHANGED
|
@@ -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.
|
package/dist/codex-backend.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { anthropicToResponsesRequest, createResponsesSSEParser, formatResponsesAnthropicSSE,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|