@askalf/dario 6.0.36 → 6.0.38

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.
@@ -221,7 +221,14 @@ export type ResponsesToolChoice = 'auto' | 'none' | 'required' | {
221
221
  name: string;
222
222
  };
223
223
  export interface ResponsesReasoningConfig {
224
- effort?: 'low' | 'medium' | 'high';
224
+ /**
225
+ * The backend accepts more levels than the thinking-budget mapping can
226
+ * produce: probed 2026-08-29 against a live account, `none, minimal, low,
227
+ * medium, high, xhigh, max` are all valid and `ultra` 400s. The wider set
228
+ * is reachable only when a caller names an effort outright (dario#1260);
229
+ * `thinkingToReasoningEffort` still only ever yields low/medium/high.
230
+ */
231
+ effort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
225
232
  summary?: 'auto' | 'concise' | 'detailed';
226
233
  }
227
234
  export interface ResponsesRequest {
@@ -338,9 +345,13 @@ export interface ResponsesResponse {
338
345
  * (max_output_tokens caps reasoning + output combined on the Responses API).
339
346
  */
340
347
  export declare const REASONING_HEADROOM: {
348
+ readonly none: 0;
349
+ readonly minimal: 6000;
341
350
  readonly low: 12000;
342
351
  readonly medium: 25000;
343
352
  readonly high: 50000;
353
+ readonly xhigh: 80000;
354
+ readonly max: 120000;
344
355
  };
345
356
  /** gpt-5.x / o-series output ceiling (tokens). */
346
357
  export declare const RESPONSES_MAX_OUTPUT_CAP = 128000;
@@ -352,6 +363,14 @@ export interface AnthropicToResponsesOptions {
352
363
  * Pass `null` to omit `summary` entirely.
353
364
  */
354
365
  reasoningSummary?: 'auto' | 'concise' | 'detailed' | null;
366
+ /**
367
+ * Force `reasoning.effort`, overriding whatever `thinking.budget_tokens`
368
+ * would have implied. Set from a model-name effort suffix on the Codex
369
+ * route (dario#1260, `gpt-5.6-terra:high`), which is the only way an
370
+ * Anthropic-shape caller can choose an effort the budget thresholds cannot
371
+ * express. Unset means the existing thinking-derived behaviour, unchanged.
372
+ */
373
+ effort?: ResponsesReasoningConfig['effort'];
355
374
  /**
356
375
  * `store`. Default false — dario is stateless and keeps no server-side
357
376
  * conversation. Set true only if a caller wants OpenAI-side retention.
@@ -312,7 +312,17 @@ function translateToolChoice(choice) {
312
312
  * client's intended visible-output budget survives on a reasoning model
313
313
  * (max_output_tokens caps reasoning + output combined on the Responses API).
314
314
  */
315
- export const REASONING_HEADROOM = { low: 12000, medium: 25000, high: 50000 };
315
+ export const REASONING_HEADROOM = {
316
+ none: 0, minimal: 6000, low: 12000, medium: 25000, high: 50000,
317
+ // dario#1260 widened the reachable levels beyond what the thinking-budget
318
+ // mapping produces. xhigh and max reason MUCH harder — measured through
319
+ // dario on one identical prompt, same model, only the level changing:
320
+ // low 1,982 output tokens, high 3,947, max 11,160. Reserve above the trend
321
+ // rather than on it: under-reserving is the failure this table exists to
322
+ // prevent (reasoning eats the whole budget, status incomplete, empty turn),
323
+ // and both values are clamped by RESPONSES_MAX_OUTPUT_CAP anyway.
324
+ xhigh: 80000, max: 120000,
325
+ };
316
326
  /** gpt-5.x / o-series output ceiling (tokens). */
317
327
  export const RESPONSES_MAX_OUTPUT_CAP = 128000;
318
328
  /**
@@ -376,7 +386,10 @@ export function anthropicToResponsesRequest(body, targetModel, options = {}) {
376
386
  if (body.tool_choice?.disable_parallel_tool_use === true && out.tools) {
377
387
  out.parallel_tool_calls = false;
378
388
  }
379
- const effort = thinkingToReasoningEffort(body.thinking);
389
+ // An explicitly named effort wins over the thinking-budget mapping: the
390
+ // caller asked for a level, not a budget, and the thresholds cannot express
391
+ // xhigh or max at all.
392
+ const effort = options.effort ?? thinkingToReasoningEffort(body.thinking);
380
393
  if (effort) {
381
394
  out.reasoning = { effort };
382
395
  const summary = options.reasoningSummary === undefined ? 'auto' : options.reasoningSummary;
@@ -1,7 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { CodexAccountCredentials } from './codex-accounts.js';
3
- import { type ResponsesUsage } from './anthropic-responses-translate.js';
3
+ import { type ResponsesReasoningConfig, type ResponsesUsage } from './anthropic-responses-translate.js';
4
4
  import { type ModelResolver, type ClaudeTarget } from './claude-model.js';
5
+ import { type EffortValue } from './effort.js';
5
6
  export declare const CODEX_BACKEND_BASE_URL: string;
6
7
  /**
7
8
  * Client version sent on the model-discovery call. The backend REQUIRES the
@@ -72,7 +73,13 @@ export declare function isCodexModel(model: string, slugs: readonly string[]): b
72
73
  * own. A single-entry chain keeps the pre-6.0 meaning exactly, so configs
73
74
  * written before this release behave identically.
74
75
  */
75
- export declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): string | null;
76
+ export interface CodexTarget {
77
+ /** The slug as the account lists it — what goes in the outbound body. */
78
+ model: string;
79
+ /** Effort the entry declared through a `:high`-style suffix, if any. */
80
+ effort?: EffortValue;
81
+ }
82
+ export declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): CodexTarget | null;
76
83
  export declare function pickClaudeFallback(models: readonly string[], slugs: readonly string[], bases?: readonly string[], resolve?: ModelResolver): string | null;
77
84
  /**
78
85
  * {@link pickClaudeFallback} with the effort the winning entry asked for.
@@ -276,4 +283,10 @@ export declare function buildCodexHeaders(creds: CodexAccountCredentials): Recor
276
283
  * testable without network (test/codex-backend.mjs), matching the pattern
277
284
  * test/codex-oauth.mjs already uses.
278
285
  */
279
- export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean, onDone?: (outcome: CodexForwardOutcome) => void, onDecline?: (info: CodexDecline) => void): Promise<boolean>;
286
+ export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean, onDone?: (outcome: CodexForwardOutcome) => void, onDecline?: (info: CodexDecline) => void,
287
+ /**
288
+ * Effort named by a model-name suffix (dario#1260). Anthropic-shape only:
289
+ * a chat/completions caller sets `reasoning_effort` itself and that already
290
+ * translates. Undefined leaves the request exactly as it was.
291
+ */
292
+ effort?: ResponsesReasoningConfig['effort']): Promise<boolean>;
@@ -24,6 +24,7 @@
24
24
  import { createHash } from 'node:crypto';
25
25
  import { anthropicToResponsesRequest, anthropicUsageFromResponses, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
26
26
  import { resolveClaudeTarget } from './claude-model.js';
27
+ import { parseEffortSuffix } from './effort.js';
27
28
  import { BAKED_BASE_MODELS } from './model-catalog.js';
28
29
  import { parseRetryAfterMs } from './provider-cooldown.js';
29
30
  export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
@@ -121,20 +122,28 @@ export function isCodexModel(model, slugs) {
121
122
  const m = model.toLowerCase();
122
123
  return slugs.some(s => s.toLowerCase() === m);
123
124
  }
124
- /**
125
- * A pool-fallback value may name a CHAIN — `gpt-5.6-sol,claude-sonnet-5` — and
126
- * each provider takes the first entry it can actually serve. These two pickers
127
- * are the whole selection rule, kept pure so it is testable without a socket.
128
- *
129
- * Reading the chain from both ends is what makes failover SYMMETRIC in v6.0.0:
130
- * `pickCodexFallback` catches a drained Claude pool, `pickClaudeFallback`
131
- * catches a ChatGPT subscription that is rate-limited or down. Neither
132
- * subscription hitting its ceiling can take the whole deployment dark on its
133
- * own. A single-entry chain keeps the pre-6.0 meaning exactly, so configs
134
- * written before this release behave identically.
135
- */
136
125
  export function pickCodexFallback(models, slugs) {
137
- return models.find(m => isCodexModel(m, slugs)) ?? null;
126
+ for (const m of models) {
127
+ // The name AS WRITTEN wins, and only a name that matches no slug at all is
128
+ // re-read as `model:effort` — the same two-pass rule resolveClaudeTarget
129
+ // uses for chain entries (dario#1161), so a slug that genuinely ends in an
130
+ // effort word keeps priority over the suffix reading.
131
+ //
132
+ // Without the second pass a codex chain entry carrying the very suffix the
133
+ // Claude half accepts (`gpt-5.6-terra:high`) matched nothing and was
134
+ // silently SKIPPED: no error, no log, just a failover the operator
135
+ // configured that never fired (dario#1260).
136
+ //
137
+ // Per entry rather than two passes over the whole list, because the chain is
138
+ // priority-ordered and a full as-written sweep would let a later entry
139
+ // overtake an earlier one purely for being spelled without a suffix.
140
+ if (isCodexModel(m, slugs))
141
+ return { model: m };
142
+ const eff = parseEffortSuffix(m);
143
+ if (eff.effort && isCodexModel(eff.model, slugs))
144
+ return { model: eff.model, effort: eff.effort };
145
+ }
146
+ return null;
138
147
  }
139
148
  export function pickClaudeFallback(models, slugs, bases = BAKED_BASE_MODELS, resolve) {
140
149
  // "Not a codex slug" is NOT the same as "the Claude pool can serve it". A
@@ -746,7 +755,13 @@ export function buildCodexHeaders(creds) {
746
755
  * testable without network (test/codex-backend.mjs), matching the pattern
747
756
  * test/codex-oauth.mjs already uses.
748
757
  */
749
- export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false, onDone, onDecline) {
758
+ export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false, onDone, onDecline,
759
+ /**
760
+ * Effort named by a model-name suffix (dario#1260). Anthropic-shape only:
761
+ * a chat/completions caller sets `reasoning_effort` itself and that already
762
+ * translates. Undefined leaves the request exactly as it was.
763
+ */
764
+ effort) {
750
765
  void req;
751
766
  const isAnthropic = shape === 'anthropic';
752
767
  // Reported exactly once, on every exit that answered the client. Without
@@ -789,7 +804,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
789
804
  const model = String(parsed.model ?? '');
790
805
  // stream is forced: the backend is always streamed and collapsed here.
791
806
  const upstreamBody = isAnthropic
792
- ? { ...anthropicToResponsesRequest(parsed, model), stream: true }
807
+ ? { ...anthropicToResponsesRequest(parsed, model, effort ? { effort } : {}), stream: true }
793
808
  : chatCompletionsToResponses(parsed);
794
809
  const scrubbed = toCodexSupportedBody({
795
810
  ...upstreamBody,
package/dist/effort.d.ts CHANGED
@@ -18,6 +18,20 @@
18
18
  /** Valid values for the `--effort` flag. Mirrors CC's effort set (`low|medium|high|xhigh|max`) plus CC's `ultracode` mode and dario's pseudo-value `'client'` for passthrough. `'ultracode'` is CC's xhigh-plus-dynamic-workflow-orchestration mode (CC 2.1.154); the Messages API accepts only low|medium|high|xhigh|max, so dario normalizes ultracode → 'xhigh' on the wire (see normalizeEffortForWire). `'client'` passes through the client's own `output_config.effort` (falling back to `'xhigh'`). dario#87, `'max'` added in dario#190, `'ultracode'` added 2026-05-28. */
19
19
  export type EffortValue = 'low' | 'medium' | 'high' | 'xhigh' | 'ultracode' | 'max' | 'client';
20
20
  export declare const VALID_EFFORT_VALUES: ReadonlyArray<EffortValue>;
21
+ /**
22
+ * dario's effort tiers onto the levels the Codex Responses backend accepts
23
+ * (dario#1260). Probed 2026-08-29: `none, minimal, low, medium, high, xhigh,
24
+ * max` are valid upstream; `ultra` 400s.
25
+ *
26
+ * - low/medium/high/xhigh/max pass through — same name on both sides.
27
+ * - `ultracode` is dario's own tier with no upstream equivalent. It maps to
28
+ * `max`, the nearest thing the backend has, rather than being dropped:
29
+ * a caller who asked for the most effort available should not silently
30
+ * get the default.
31
+ * - `client` means "whatever the client asked for", so it forces nothing
32
+ * and the request's own thinking budget decides, as before.
33
+ */
34
+ export declare function effortForCodex(effort: EffortValue | undefined): 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
21
35
  export declare function parseEffortSuffix(model: string): {
22
36
  model: string;
23
37
  effort?: EffortValue;
package/dist/effort.js CHANGED
@@ -26,6 +26,32 @@ export const VALID_EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'ultracode
26
26
  * suffix removed plus the parsed effort (undefined when none). Exported for tests.
27
27
  */
28
28
  const SUFFIX_EFFORTS = ['ultracode', 'medium', 'xhigh', 'high', 'low', 'max'];
29
+ /**
30
+ * dario's effort tiers onto the levels the Codex Responses backend accepts
31
+ * (dario#1260). Probed 2026-08-29: `none, minimal, low, medium, high, xhigh,
32
+ * max` are valid upstream; `ultra` 400s.
33
+ *
34
+ * - low/medium/high/xhigh/max pass through — same name on both sides.
35
+ * - `ultracode` is dario's own tier with no upstream equivalent. It maps to
36
+ * `max`, the nearest thing the backend has, rather than being dropped:
37
+ * a caller who asked for the most effort available should not silently
38
+ * get the default.
39
+ * - `client` means "whatever the client asked for", so it forces nothing
40
+ * and the request's own thinking budget decides, as before.
41
+ */
42
+ export function effortForCodex(effort) {
43
+ switch (effort) {
44
+ case 'low':
45
+ case 'medium':
46
+ case 'high':
47
+ case 'xhigh':
48
+ case 'max': return effort;
49
+ case 'ultracode': return 'max';
50
+ case 'client':
51
+ case undefined: return undefined;
52
+ default: return undefined;
53
+ }
54
+ }
29
55
  export function parseEffortSuffix(model) {
30
56
  for (const e of SUFFIX_EFFORTS) {
31
57
  for (const sep of [':', '-']) {
package/dist/proxy.js CHANGED
@@ -23,7 +23,8 @@ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncL
23
23
  import { handleAdminRequest } from './admin-api.js';
24
24
  import { createTokenBucket } from './rate-limit.js';
25
25
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
26
- import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
26
+ import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
27
+ import { effortForCodex } from './effort.js';
27
28
  import { isClaudeServableModel } from './claude-model.js';
28
29
  import { MODEL_UNROUTABLE } from './upstream-rejection.js';
29
30
  import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
@@ -1945,9 +1946,10 @@ export async function startProxy(opts = {}) {
1945
1946
  return false;
1946
1947
  }
1947
1948
  const slugs = await getCodexModelSlugs(creds).catch(() => []);
1948
- const fallbackModel = pickCodexFallback(fallbackModels, slugs);
1949
- if (!fallbackModel)
1949
+ const fallbackPick = pickCodexFallback(fallbackModels, slugs);
1950
+ if (!fallbackPick)
1950
1951
  return false;
1952
+ const fallbackModel = fallbackPick.model;
1951
1953
  const fallbackBody = buildPoolFallbackBody(body, fallbackModel);
1952
1954
  if (!fallbackBody)
1953
1955
  return false;
@@ -1969,7 +1971,12 @@ export async function startProxy(opts = {}) {
1969
1971
  // an outage, not quota — cooling it would park a provider that may be
1970
1972
  // back on the next request, which is the opposite of the fix.
1971
1973
  (d) => { if (d.status === 429)
1972
- providerCooldowns.note('codex', d.retryAfterMs); });
1974
+ providerCooldowns.note('codex', d.retryAfterMs); },
1975
+ // The mirror of the Claude side (dario#1161): an operator who writes
1976
+ // `--pool-fallback=gpt-5.6-terra:high` is choosing the effort the
1977
+ // failover runs at, so the entry's own suffix reaches the request rather
1978
+ // than the failover quietly running at the backend default.
1979
+ effortForCodex(fallbackPick.effort));
1973
1980
  if (served)
1974
1981
  providerCooldowns.clear('codex');
1975
1982
  return served;
@@ -3066,9 +3073,9 @@ export async function startProxy(opts = {}) {
3066
3073
  if (body.length > 0) {
3067
3074
  try {
3068
3075
  const peek = (parsedBody ?? {}); // parsed once by the invalid-body guard; `body` is re-serialized from it
3069
- const rawModel = (peek.model || '').toString();
3070
- const requestPoolFallbackModels = selectPoolFallbackModels(poolFallbackSpec, rawModel);
3071
- const requestPoolFallbackModel = requestPoolFallbackModels[0] ?? null;
3076
+ // Reassignable: the codex effort-suffix strip below rewrites it, and
3077
+ // every routing decision after that point must see the stripped name.
3078
+ let rawModel = (peek.model || '').toString();
3072
3079
  // Credentials are re-read per request (not cached at startup) because
3073
3080
  // a refresh rotates them on disk; getFreshCodexAccount refreshes when
3074
3081
  // inside the expiry buffer, collapsing concurrent refreshes per alias.
@@ -3105,6 +3112,43 @@ export async function startProxy(opts = {}) {
3105
3112
  }
3106
3113
  }
3107
3114
  }
3115
+ // dario#1260 — an effort suffix on a CODEX model (`gpt-5.6-terra:high`).
3116
+ // The two strip sites further up deliberately leave OpenAI-shaped names
3117
+ // alone: an openai-compat backend may serve a model whose real id ends
3118
+ // in `-high`/`-low`, and stripping there would rewrite a legitimate
3119
+ // name against a catalog this proxy cannot see. Codex is the one
3120
+ // provider that publishes its routable set, so here — and only here —
3121
+ // the ambiguity is decidable: strip when the name as written matches no
3122
+ // slug and the stripped name matches one.
3123
+ //
3124
+ // That guard is what makes this strictly additive. The only requests
3125
+ // whose routing changes are the ones that answer 400 `model_unroutable`
3126
+ // today, because a name that already routes is never re-read.
3127
+ //
3128
+ // Placed HERE rather than beside its siblings because `codexModels` is
3129
+ // THIS request's account's list, resolved just above; the suffix cannot
3130
+ // be told apart from a real id without it.
3131
+ if (rawModel && codexModels.length > 0 && !isCodexModel(rawModel, codexModels)) {
3132
+ const eff = parseEffortSuffix(rawModel);
3133
+ if (eff.effort && isCodexModel(eff.model, codexModels)) {
3134
+ if (verbose)
3135
+ console.log(`[dario] effort suffix: ${rawModel} → model ${eff.model} (codex, effort: ${eff.effort})`);
3136
+ requestEffort = eff.effort;
3137
+ rawModel = eff.model;
3138
+ // The suffix must not survive into the outbound body: the backend
3139
+ // 400s on a slug it does not list, which is the very failure this
3140
+ // fixes. `body` is re-serialized from `parsedBody` exactly as the
3141
+ // alias and provider-prefix blocks above do.
3142
+ peek.model = eff.model;
3143
+ body = Buffer.from(JSON.stringify(parsedBody));
3144
+ }
3145
+ }
3146
+ // Chosen AFTER the strip above, not before it: a per-model fallback
3147
+ // spec is keyed on the model being routed, and keying it on a name
3148
+ // still carrying a dario-side effort suffix would miss the operator's
3149
+ // own entry for that model and fall back to the unscoped chain.
3150
+ const requestPoolFallbackModels = selectPoolFallbackModels(poolFallbackSpec, rawModel);
3151
+ const requestPoolFallbackModel = requestPoolFallbackModels[0] ?? null;
3108
3152
  const decision = routeProvider({
3109
3153
  isOpenAIPath: isOpenAI,
3110
3154
  model: rawModel,
@@ -3205,7 +3249,11 @@ export async function startProxy(opts = {}) {
3205
3249
  // is an outage, and parking a provider for that would keep it out
3206
3250
  // of the chain while it was already coming back.
3207
3251
  (d) => { if (d.status === 429)
3208
- providerCooldowns.note('codex', d.retryAfterMs); });
3252
+ providerCooldowns.note('codex', d.retryAfterMs); },
3253
+ // dario#1260 — the effort named by the model-name suffix stripped
3254
+ // above. Undefined for every request that did not name one, which
3255
+ // leaves the outbound body exactly as it was.
3256
+ effortForCodex(requestEffort));
3209
3257
  if (served) {
3210
3258
  // A provider that just served is not rate-limited.
3211
3259
  providerCooldowns.clear('codex');
package/dist/version.js CHANGED
@@ -1,28 +1,42 @@
1
1
  /**
2
- * dario's own package version, read once from the bundled package.json.
2
+ * dario's own package version, bound at module load.
3
3
  *
4
4
  * Surfaced on `/status` and `/health` (#640) so a headless operator can confirm
5
5
  * an auto-update actually rolled the running proxy — `curl /health | jq .version`
6
6
  * beats exec-ing into the container to read package.json.
7
+ *
8
+ * WHY AT MODULE LOAD, NOT ON FIRST CALL. This used to read package.json lazily
9
+ * the first time someone asked, and cache that. `npm i -g` rewrites
10
+ * package.json under the running install without touching the process, so a
11
+ * proxy that had not served `/status` before an upgrade answered its first one
12
+ * with the NEW version while still executing the OLD code — precisely the
13
+ * opposite of what the field exists to report. It cost the reporter on #1244 a
14
+ * round trip: `/status` read 6.0.34 while `GET /admin/accounts` was still
15
+ * emitting the 6.0.33 field set, so the advice he had been given ("upgrade,
16
+ * then read `organization_id`") looked already done.
17
+ *
18
+ * Reading at import binds the value to the process. proxy.ts imports this at
19
+ * startup, so `/status` reports the build that is actually answering until it
20
+ * restarts — the only claim the field can honestly make.
7
21
  */
8
22
  import { readFileSync } from 'node:fs';
9
23
  import { join, dirname } from 'node:path';
10
24
  import { fileURLToPath } from 'node:url';
11
- let cached = null;
12
- export function darioVersion() {
13
- if (cached !== null)
14
- return cached;
15
- let v = 'unknown';
25
+ function readVersion() {
16
26
  try {
17
27
  // dist/version.js → ../package.json (same layout the MCP server + CLI use).
18
28
  const here = dirname(fileURLToPath(import.meta.url));
19
29
  const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8'));
20
30
  if (typeof pkg.version === 'string')
21
- v = pkg.version;
31
+ return pkg.version;
22
32
  }
23
33
  catch {
24
- // package.json missing/malformed — keep 'unknown', never throw.
34
+ // package.json missing/malformed — report 'unknown', never throw.
25
35
  }
26
- cached = v;
27
- return v;
36
+ return 'unknown';
37
+ }
38
+ /** Read once, at import. See the note above for why not on first call. */
39
+ const VERSION = readVersion();
40
+ export function darioVersion() {
41
+ return VERSION;
28
42
  }
@@ -74,6 +74,39 @@ Off by default, each one a deliberate divergence from what real CC sends.
74
74
  | `DARIO_EFFORT` | `--effort=` | Forces a reasoning-effort level. Can flip requests to overage billing — watch `-v` logs for representative-claim changes ([`#87`](https://github.com/askalf/dario/issues/87)). |
75
75
  | `DARIO_MAX_TOKENS` | `--max-tokens=` | Anthropic enforces the per-model ceiling server-side, so too-high values return a clean 400 ([`#88`](https://github.com/askalf/dario/issues/88)). |
76
76
 
77
+ ### Per-request effort, by model name
78
+
79
+ `DARIO_EFFORT` is process-wide: it applies to every caller, Claude and Codex
80
+ alike. A client that has no way to set `output_config.effort` can instead name
81
+ the level in the model itself, and only that request changes:
82
+
83
+ ```
84
+ claude-opus-4-8:high colon form
85
+ claude-opus-4-8-high hyphen form, for Cursor, which rewrites colons
86
+ gpt-5.6-terra:high a Codex model, same two spellings
87
+ ```
88
+
89
+ Levels a suffix may name: `low`, `medium`, `high`, `xhigh`, `max`, and dario's
90
+ own `ultracode`, which reaches a Codex backend as `max`. On the Codex path the
91
+ level is sent as `reasoning.effort`; on the Claude path as
92
+ `output_config.effort`.
93
+
94
+ `client` is a valid `DARIO_EFFORT` value but deliberately **not** a suffix. It
95
+ means "leave the client's own choice alone", so naming it in a model would be a
96
+ no-op, and accepting it would let a model genuinely named `...-client` be
97
+ stripped to a name the backend does not list.
98
+
99
+ The suffix is only read when the name as written matches no model the provider
100
+ lists, so a real model id that happens to end in an effort word is routed
101
+ exactly as written and never re-read. A `--pool-fallback` chain entry may carry
102
+ one too, which sets the effort the failover request runs at.
103
+
104
+ Effort is not free. Measured through dario on one prompt with only the level
105
+ changing: `low` returned 1,982 output tokens in 18s, `high` 3,947 in 35s, and
106
+ `max` 11,160 in 100s. Reasoning is billed as output, so `max` is a real cost
107
+ increase on every request that names it, and on a long agent loop it can push a
108
+ run into its own timeout.
109
+
77
110
  ## Pacing
78
111
 
79
112
  Only meaningful with stealth, and all default to 0 (off) except the cap. See [`wire-fidelity.md`](./wire-fidelity.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.0.36",
3
+ "version": "6.0.38",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {