@askalf/dario 6.2.1 → 6.4.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/proxy.js CHANGED
@@ -24,9 +24,10 @@ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncL
24
24
  import { handleAdminRequest } from './admin-api.js';
25
25
  import { createTokenBucket } from './rate-limit.js';
26
26
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
27
- import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
27
+ import { forwardToCodex, forwardResponsesToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
28
28
  import { effortForCodex } from './effort.js';
29
29
  import { MidstreamGuard, guardFor, loopbackBaseFor, chaosCutFetch, chaosCutState, CONTINUATION_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth } from './midstream.js';
30
+ import { responsesRequestToAnthropic, unsupportedOnClaudeError, ResponsesRequestError, ResponsesOut, wrapResponsesClient } from './responses-inbound.js';
30
31
  import { isClaudeServableModel } from './claude-model.js';
31
32
  import { MODEL_UNROUTABLE } from './upstream-rejection.js';
32
33
  import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
@@ -609,6 +610,9 @@ export function resolveProxyTarget(urlPath, isOpenAI) {
609
610
  return { target: `${ANTHROPIC_API}/v1/messages?beta=true`, thin: false };
610
611
  const allowed = {
611
612
  '/v1/messages': { target: `${ANTHROPIC_API}/v1/messages?beta=true`, thin: false },
613
+ // OpenAI Responses shape (v6.3, src/responses-inbound.ts): translated to
614
+ // a Messages body at the front door, served like any Anthropic request.
615
+ '/v1/responses': { target: `${ANTHROPIC_API}/v1/messages?beta=true`, thin: false },
612
616
  '/v1/messages/count_tokens': { target: `${ANTHROPIC_API}/v1/messages/count_tokens`, thin: true },
613
617
  '/v1/complete': { target: `${ANTHROPIC_API}/v1/complete`, thin: false },
614
618
  };
@@ -2602,6 +2606,17 @@ export async function startProxy(opts = {}) {
2602
2606
  }
2603
2607
  // Detect OpenAI-format requests
2604
2608
  const isOpenAI = urlPath === '/v1/chat/completions';
2609
+ // A Responses-API client (Codex CLI, the OpenAI Agents SDK). Its request
2610
+ // becomes an Anthropic Messages body below and everything written back to
2611
+ // it is translated at the write boundary — from here on `res` IS that
2612
+ // boundary, so even a pre-upstream error reaches the client in its shape.
2613
+ const isResponses = urlPath === '/v1/responses';
2614
+ const responsesOut = isResponses ? new ResponsesOut('') : null;
2615
+ // The untranslated response, for the one path that answers a Responses
2616
+ // client in its own shape without translation: the codex passthrough.
2617
+ const rawRes = res;
2618
+ if (responsesOut)
2619
+ res = wrapResponsesClient(res, responsesOut);
2605
2620
  // Allowlisted API paths — only these are proxied (prevents SSRF).
2606
2621
  // count_tokens forwards thin (no template injection) — see resolveProxyTarget.
2607
2622
  const route = resolveProxyTarget(urlPath, isOpenAI);
@@ -2966,7 +2981,7 @@ export async function startProxy(opts = {}) {
2966
2981
  // (aliases, prefixes, the CC template); a mid-stream continuation
2967
2982
  // re-issues the CLIENT's request, not the rewritten one, so dario's own
2968
2983
  // rules apply to the resume the same way they applied to the original.
2969
- const clientBodyBytes = body;
2984
+ let clientBodyBytes = body;
2970
2985
  // How deep in a continuation chain this request sits: 0 for a client
2971
2986
  // request, 1 for its resume, 2 for the resume of that resume — which is
2972
2987
  // never continued itself (MAX_CONTINUATION_DEPTH).
@@ -3084,6 +3099,45 @@ export async function startProxy(opts = {}) {
3084
3099
  return;
3085
3100
  }
3086
3101
  }
3102
+ // Responses shape → Messages shape, once, before any routing peeks at
3103
+ // the body. The translated body is what a continuation re-issues too:
3104
+ // the loopback goes to /v1/messages, which is what this body now is.
3105
+ // The client's Responses body as written — a ChatGPT-subscription model
3106
+ // gets it verbatim (forwardResponsesToCodex), every other route gets the
3107
+ // translation.
3108
+ let responsesBodyRaw = null;
3109
+ // NOTHING IS REFUSED HERE. Routing has not happened yet, so the
3110
+ // translation only RECORDS what the Messages shape cannot carry
3111
+ // (`t.unsupported`, e.g. previous_response_id). The route decides: the
3112
+ // codex passthrough below forwards `responsesBodyRaw` untouched, so a
3113
+ // stateful follow-up on a ChatGPT-subscription model reaches the backend
3114
+ // that keeps state; only the Claude path, after the codex branch has
3115
+ // passed on the request, answers a 400 naming the field. Both halves are
3116
+ // asserted in test/responses-inbound-wiring.mjs ("previous_response_id
3117
+ // on a ChatGPT-subscription model → forwarded untouched").
3118
+ let responsesUnsupported = [];
3119
+ if (isResponses && parsedBody !== null) {
3120
+ try {
3121
+ responsesBodyRaw = parsedBody;
3122
+ const t = responsesRequestToAnthropic(parsedBody);
3123
+ if (verbose && t.warnings.length > 0)
3124
+ console.log(`[dario] #${requestCount} /v1/responses: ${t.warnings.join('; ')}`);
3125
+ responsesUnsupported = t.unsupported;
3126
+ parsedBody = t.body;
3127
+ body = Buffer.from(JSON.stringify(t.body));
3128
+ clientBodyBytes = body;
3129
+ }
3130
+ catch (err) {
3131
+ // Only the translator's own verdicts reach the client; anything else
3132
+ // is an internal failure and says so without its message.
3133
+ const known = err instanceof ResponsesRequestError;
3134
+ if (!known && verbose)
3135
+ console.error(`[dario] #${requestCount} /v1/responses translation failed: ${sanitizeError(err)}`);
3136
+ res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
3137
+ res.end(JSON.stringify({ error: { message: known ? err.message : 'request could not be translated', type: 'invalid_request_error', param: known ? err.param ?? null : null, code: null } }));
3138
+ return;
3139
+ }
3140
+ }
3087
3141
  // Provider prefix (v3.10.0). If the body's model field is `<provider>:<model>`
3088
3142
  // with a recognized prefix, strip the prefix and force routing regardless of
3089
3143
  // regex. CLI-level `--model=<provider>:<name>` applies the same override
@@ -3412,14 +3466,13 @@ export async function startProxy(opts = {}) {
3412
3466
  },
3413
3467
  })
3414
3468
  : null;
3415
- const served = codexAvailable && await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', codexFetch, canDefer,
3416
3469
  // Before this hook a codex request left no trace: nothing in
3417
3470
  // /analytics, nothing in the request log, no per-account count.
3418
3471
  // The dock (and anyone reading /analytics) saw a proxy that
3419
3472
  // served GPT all day and reported zero of it. A decline (the
3420
3473
  // request handed to the Claude pool) reports nothing here; the
3421
3474
  // Claude path records what it then serves.
3422
- (o) => {
3475
+ const codexOnDone = (o) => {
3423
3476
  codexRequestCounts.set(o.alias, (codexRequestCounts.get(o.alias) ?? 0) + 1);
3424
3477
  analytics.record({
3425
3478
  timestamp: Date.now(),
@@ -3448,16 +3501,25 @@ export async function startProxy(opts = {}) {
3448
3501
  inputTokens: o.inputTokens, outputTokens: o.outputTokens,
3449
3502
  cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
3450
3503
  }, consumer));
3451
- },
3452
- // Cool codex on a rate limit only — a 5xx or an unreachable backend
3453
- // is an outage, and parking a provider for that would keep it out
3454
- // of the chain while it was already coming back.
3455
- (d) => { if (d.status === 429)
3456
- providerCooldowns.note('codex', d.retryAfterMs); },
3457
- // dario#1260 the effort named by the model-name suffix stripped
3458
- // above. Undefined for every request that did not name one, which
3459
- // leaves the outbound body exactly as it was.
3460
- effortForCodex(requestEffort), codexGuard);
3504
+ };
3505
+ // A Responses client on a ChatGPT-subscription model: the backend
3506
+ // speaks that shape natively, so the body goes through as written
3507
+ // (model resolved) and the SSE comes back untouched no round
3508
+ // trip through the Messages shape, which cannot carry the newest
3509
+ // Codex CLI request features. Answers on the raw response: these
3510
+ // bytes are already in the client's shape.
3511
+ const served = codexAvailable && (isResponses && responsesBodyRaw
3512
+ ? await forwardResponsesToCodex(rawRes, { ...responsesBodyRaw, model: rawModel }, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, codexFetch, codexOnDone)
3513
+ : await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', codexFetch, canDefer, codexOnDone,
3514
+ // Cool codex on a rate limit only — a 5xx or an unreachable backend
3515
+ // is an outage, and parking a provider for that would keep it out
3516
+ // of the chain while it was already coming back.
3517
+ (d) => { if (d.status === 429)
3518
+ providerCooldowns.note('codex', d.retryAfterMs); },
3519
+ // dario#1260 — the effort named by the model-name suffix stripped
3520
+ // above. Undefined for every request that did not name one, which
3521
+ // leaves the outbound body exactly as it was.
3522
+ effortForCodex(requestEffort), codexGuard));
3461
3523
  if (served) {
3462
3524
  // A provider that just served is not rate-limited.
3463
3525
  providerCooldowns.clear('codex');
@@ -3547,6 +3609,16 @@ export async function startProxy(opts = {}) {
3547
3609
  }
3548
3610
  catch { /* not JSON — fall through to existing path */ }
3549
3611
  }
3612
+ // A Responses feature only the codex passthrough can honour, on a
3613
+ // request the Claude pool is about to serve: refuse it by name here,
3614
+ // after routing, so the same field on a ChatGPT-subscription model was
3615
+ // forwarded untouched above.
3616
+ if (isResponses && responsesUnsupported.length > 0) {
3617
+ requestCount++;
3618
+ res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
3619
+ res.end(JSON.stringify(unsupportedOnClaudeError(responsesUnsupported[0])));
3620
+ return;
3621
+ }
3550
3622
  // Claude's turn: the routing block above declined this request, so it
3551
3623
  // needs a pool account. Selecting HERE and not before the body read is
3552
3624
  // the fix for dario#1137 — a ChatGPT-subscription-only user has a
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Inbound OpenAI Responses API (v6.3) — `POST /v1/responses` on dario.
3
+ *
4
+ * Codex CLI 0.154 dropped `wire_api = "chat"` (openai/codex discussion 7782):
5
+ * a custom provider must speak the Responses API or it cannot be used at all.
6
+ * The OpenAI Agents SDK and everything newer from OpenAI speak the same shape.
7
+ * This module makes dario a Responses endpoint, so those clients run on a
8
+ * Claude subscription — and on a ChatGPT one, through the codex leg.
9
+ *
10
+ * Shape of the work: the request is translated ONCE at the front door into
11
+ * the Anthropic Messages body every other dario path already understands
12
+ * (`responsesRequestToAnthropic`), the request then runs as an ordinary
13
+ * Anthropic-shape request — pool, template, codex leg, mid-stream
14
+ * continuation, all of it — and every byte written back to the client passes
15
+ * through `ResponsesOut`, which turns Anthropic SSE (or a buffered Anthropic
16
+ * message, or an Anthropic error body) into the Responses wire shape. Nothing
17
+ * downstream of the front door knows the client is a Responses client.
18
+ *
19
+ * The reverse direction — an Anthropic-shape request served by the codex
20
+ * backend — has lived in anthropic-responses-translate.ts since 5.5.87. The
21
+ * two translators share types and nothing else on purpose: each direction is
22
+ * read against the wire captures that motivated it.
23
+ *
24
+ * What is dropped, and said so once per process at verbose: hosted tool types
25
+ * the pool cannot run (`web_search`, `file_search`, `mcp`, …), `reasoning`
26
+ * items on the way in (the encrypted content is OpenAI's, and the pool does
27
+ * not need them back), `text.format`, `previous_response_id` (dario is
28
+ * stateless; a 400, not a silent ignore).
29
+ */
30
+ import type { ServerResponse } from 'node:http';
31
+ export interface InboundTranslation {
32
+ body: Record<string, unknown>;
33
+ /** Things that did not survive the translation, one line each. */
34
+ warnings: string[];
35
+ /**
36
+ * Request features the Messages shape has no honest answer for — the
37
+ * route that serves the request decides what to do: the codex passthrough
38
+ * forwards the original body and never sees this; the Claude pool answers
39
+ * a 400 naming the field rather than silently ignoring it.
40
+ */
41
+ unsupported: string[];
42
+ }
43
+ export declare class ResponsesRequestError extends Error {
44
+ readonly param?: string | undefined;
45
+ constructor(message: string, param?: string | undefined);
46
+ }
47
+ /**
48
+ * The Responses request as the Anthropic Messages body the rest of dario
49
+ * serves. Throws ResponsesRequestError for shapes that cannot be served
50
+ * honestly (no model, no input, `previous_response_id`).
51
+ */
52
+ export declare function responsesRequestToAnthropic(req: Record<string, unknown>): InboundTranslation;
53
+ /** The 400 the Claude pool answers for a Responses feature it cannot serve. */
54
+ export declare function unsupportedOnClaudeError(field: string): Record<string, unknown>;
55
+ /** A buffered Anthropic message → a Responses response object. */
56
+ export declare function anthropicMessageToResponses(msg: Record<string, unknown>, createdAt?: number): Record<string, unknown>;
57
+ /** An Anthropic error body → the OpenAI error envelope. */
58
+ export declare function anthropicErrorToResponses(body: Record<string, unknown>): Record<string, unknown>;
59
+ /**
60
+ * Anthropic SSE → Responses SSE, incrementally. One instance per response.
61
+ * Comments (`: dario continuation …`) ride through untouched; `ping` is
62
+ * dropped; `error` becomes `response.failed` + an `error` event.
63
+ */
64
+ export declare class ResponsesOutStream {
65
+ private seq;
66
+ private id;
67
+ private createdAt;
68
+ private model;
69
+ private started;
70
+ private readonly output;
71
+ private readonly open;
72
+ private usage;
73
+ private stopReason;
74
+ private done;
75
+ private readonly splitter;
76
+ constructor(requestModel: string);
77
+ private ev;
78
+ private snapshot;
79
+ feed(chunk: string | Uint8Array): string;
80
+ /** Whatever is still buffered (a partial frame) — nothing a Responses client can use. */
81
+ end(): string;
82
+ get finished(): boolean;
83
+ private frame;
84
+ private blockStart;
85
+ private blockDelta;
86
+ private blockStop;
87
+ }
88
+ /**
89
+ * Everything dario writes to a Responses client passes through here. The
90
+ * first bytes decide the mode: SSE frames are translated as they arrive; a
91
+ * JSON body (a buffered message, or an error) is held and translated at end().
92
+ */
93
+ export declare class ResponsesOut {
94
+ private mode;
95
+ private json;
96
+ private readonly stream;
97
+ private readonly decoder;
98
+ constructor(requestModel: string);
99
+ write(chunk: string | Uint8Array): string;
100
+ end(): string;
101
+ }
102
+ /**
103
+ * The ServerResponse a Responses client is served through: every write is
104
+ * translated, everything else reaches the real response untouched (headers,
105
+ * events, `writableEnded`, `destroyed`). Bound methods, so `res.on('close')`
106
+ * and friends keep working on the real object.
107
+ */
108
+ export declare function wrapResponsesClient(res: ServerResponse, out: ResponsesOut): ServerResponse;