@askalf/dario 6.0.4 → 6.0.5

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.
@@ -8,6 +8,7 @@
8
8
  * action required. See src/live-fingerprint.ts for the capture pipeline.
9
9
  */
10
10
  import { TemplateData, PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS } from './live-fingerprint.js';
11
+ import { VALID_EFFORT_VALUES, parseEffortSuffix, type EffortValue } from './effort.js';
11
12
  export { PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS };
12
13
  /** The loaded template itself — source, version, capture age, all fields. Startup banners and drift checks read this directly. */
13
14
  export declare const CC_TEMPLATE: TemplateData;
@@ -364,13 +365,8 @@ export declare const DEFAULT_MAX_TOKENS = 64000;
364
365
  * Sonnet; pinning to 32k silently truncated its output capacity).
365
366
  */
366
367
  export declare function resolveMaxTokens(flag: number | 'client' | undefined, clientBody: Record<string, unknown>): number;
367
- /** 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. */
368
- export type EffortValue = 'low' | 'medium' | 'high' | 'xhigh' | 'ultracode' | 'max' | 'client';
369
- export declare const VALID_EFFORT_VALUES: ReadonlyArray<EffortValue>;
370
- export declare function parseEffortSuffix(model: string): {
371
- model: string;
372
- effort?: EffortValue;
373
- };
368
+ export { VALID_EFFORT_VALUES, parseEffortSuffix };
369
+ export type { EffortValue };
374
370
  /**
375
371
  * Resolve the outbound `output_config.effort` value.
376
372
  *
@@ -8,6 +8,7 @@
8
8
  * action required. See src/live-fingerprint.ts for the capture pipeline.
9
9
  */
10
10
  import { loadTemplate, promptVariantsOf, VARIANT_FAMILIES, PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS, } from './live-fingerprint.js';
11
+ import { VALID_EFFORT_VALUES, parseEffortSuffix } from './effort.js';
11
12
  // Re-exported so existing importers (scripts/capture-and-bake.mjs, the template
12
13
  // invariant tests) keep their import site. The definitions live in
13
14
  // live-fingerprint.ts because loadTemplate must apply the same superset rule to
@@ -1183,28 +1184,14 @@ export function resolveMaxTokens(flag, clientBody) {
1183
1184
  }
1184
1185
  return flag;
1185
1186
  }
1186
- export const VALID_EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'ultracode', 'max', 'client'];
1187
- /**
1188
- * dario#419 strip an optional effort suffix off a model name, so OpenAI-compat
1189
- * clients that can't set `output_config.effort` (e.g. Cursor) can choose effort
1190
- * by model name: `opus-4-8:high` (colon) or Cursor-style `claude-opus-4-8-high`
1191
- * (hyphen). Only the wire-valid effort levels are recognized as a suffix — any
1192
- * other trailing token is left as part of the model name, and a bare model that
1193
- * IS an effort word (e.g. just "high") is left alone. Returns the model with the
1194
- * suffix removed plus the parsed effort (undefined when none). Exported for tests.
1195
- */
1196
- const SUFFIX_EFFORTS = ['ultracode', 'medium', 'xhigh', 'high', 'low', 'max'];
1197
- export function parseEffortSuffix(model) {
1198
- for (const e of SUFFIX_EFFORTS) {
1199
- for (const sep of [':', '-']) {
1200
- const tag = sep + e;
1201
- if (model.length > tag.length && model.endsWith(tag)) {
1202
- return { model: model.slice(0, -tag.length), effort: e };
1203
- }
1204
- }
1205
- }
1206
- return { model };
1207
- }
1187
+ // The effort vocabulary and the model-name effort suffix live in effort.ts so
1188
+ // the pool-fallback classifier can parse the SAME suffix the request path
1189
+ // parses without importing this module claude-model.ts stays decidable over
1190
+ // a base set alone, and hand-copying the suffix list there is what would let
1191
+ // `claude:opus:high` drift from `opus:high` (dario#1156 review). Re-exported
1192
+ // here because proxy.ts, cli.ts and test/effort-flag.mjs import them from
1193
+ // cc-template.
1194
+ export { VALID_EFFORT_VALUES, parseEffortSuffix };
1208
1195
  /**
1209
1196
  * Normalize an effort value to a wire-valid `output_config.effort`. The
1210
1197
  * Messages API accepts only low|medium|high|xhigh|max. CC's `ultracode` is a
@@ -1,12 +1,29 @@
1
+ import { type EffortValue } from './effort.js';
1
2
  /** Maps a bare name to a canonical id, or returns it unchanged. */
2
3
  export type ModelResolver = (model: string) => string;
3
4
  /**
4
- * The canonical id the Claude pool would serve `model` as, or null when it
5
- * cannot serve it. `bases` is the catalog base set `getCachedBases()` at a
6
- * call site, a fixture in a test. `resolve` is the alias pipeline; the default
7
- * knows only catalog family shorthands, so pass the proxy's full resolver
8
- * (operator aliases + pinned aliases) where one exists.
5
+ * What the pool would serve a chain entry as: the canonical id, plus the effort
6
+ * the entry ASKED FOR when it carried a suffix (`claude:opus:high`). Both
7
+ * halves travel together because the caller needs both the id goes into the
8
+ * body, the effort into the outbound `output_config` and dropping the second
9
+ * is how `claude:opus:high` reached Anthropic at the pool default (dario#1161).
10
+ */
11
+ export type ClaudeTarget = {
12
+ model: string;
13
+ effort?: EffortValue;
14
+ };
15
+ /**
16
+ * The id the Claude pool would serve `model` as AND the effort the entry asked
17
+ * for, or null when the pool cannot serve it. `bases` is the catalog base set —
18
+ * `getCachedBases()` at a call site, a fixture in a test. `resolve` is the alias
19
+ * pipeline; the default knows only catalog family shorthands, so pass the
20
+ * proxy's full resolver (operator aliases + pinned aliases) where one exists.
21
+ */
22
+ export declare function resolveClaudeTarget(model: string, bases?: readonly string[], resolve?: ModelResolver): ClaudeTarget | null;
23
+ /**
24
+ * The canonical id alone, for callers that only need to name the model. See
25
+ * resolveClaudeTarget for the effort half.
9
26
  */
10
27
  export declare function resolveClaudeServable(model: string, bases?: readonly string[], resolve?: ModelResolver): string | null;
11
- /** Whether the Claude pool can serve `model`. See resolveClaudeServable. */
28
+ /** Whether the Claude pool can serve `model`. See resolveClaudeTarget. */
12
29
  export declare function isClaudeServableModel(model: string, bases?: readonly string[], resolve?: ModelResolver): boolean;
@@ -21,7 +21,8 @@
21
21
  * supplies that resolver so classification and forwarding cannot
22
22
  * disagree. The prefix rule applies to the alias TARGET too, since an
23
23
  * operator alias is written the way a request model is written — see
24
- * stripClaudePrefix.
24
+ * stripClaudePrefix. So does the effort suffix (`:high`, `-high`) — see
25
+ * servableTarget.
25
26
  * 2. VALIDATE the resolved id against the catalog base set. A `claude-`
26
27
  * prefix on its own proves nothing — `claude-sonnet-6` has the prefix and
27
28
  * does not exist — so the id (sans `[1m]`) must be a base the pool can
@@ -37,6 +38,7 @@
37
38
  * cold catalog must still admit real ids, and must still refuse typos.
38
39
  */
39
40
  import { BAKED_BASE_MODELS, resolveAliasAgainst } from './model-catalog.js';
41
+ import { parseEffortSuffix } from './effort.js';
40
42
  /** Provider prefixes that force the Claude path (mirrors proxy.ts's PROVIDER_PREFIXES). */
41
43
  const CLAUDE_PREFIXES = new Set(['claude', 'anthropic']);
42
44
  /**
@@ -57,13 +59,29 @@ function stripClaudePrefix(model) {
57
59
  return model.slice(idx + 1) || null;
58
60
  }
59
61
  /**
60
- * The canonical id the Claude pool would serve `model` as, or null when it
61
- * cannot serve it. `bases` is the catalog base set `getCachedBases()` at a
62
- * call site, a fixture in a test. `resolve` is the alias pipeline; the default
63
- * knows only catalog family shorthands, so pass the proxy's full resolver
64
- * (operator aliases + pinned aliases) where one exists.
62
+ * One spelling of the target: strip its prefix, run the catalog pass, keep it
63
+ * only if the result is a base the pool can forward.
64
+ *
65
+ * A target that arrived prefixed still holds a shorthand (`claude:opus`
66
+ * `opus`), so the catalog pass runs on it. Idempotent for a target already
67
+ * canonical: resolveAliasAgainst only answers for family shorthands.
65
68
  */
66
- export function resolveClaudeServable(model, bases = BAKED_BASE_MODELS, resolve) {
69
+ function servableTarget(target, bases) {
70
+ const stripped = stripClaudePrefix(target);
71
+ if (stripped === null)
72
+ return null;
73
+ const resolved = resolveAliasAgainst(stripped, bases) ?? stripped;
74
+ const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
75
+ return bases.some((b) => b.toLowerCase() === base) ? resolved : null;
76
+ }
77
+ /**
78
+ * The id the Claude pool would serve `model` as AND the effort the entry asked
79
+ * for, or null when the pool cannot serve it. `bases` is the catalog base set —
80
+ * `getCachedBases()` at a call site, a fixture in a test. `resolve` is the alias
81
+ * pipeline; the default knows only catalog family shorthands, so pass the
82
+ * proxy's full resolver (operator aliases + pinned aliases) where one exists.
83
+ */
84
+ export function resolveClaudeTarget(model, bases = BAKED_BASE_MODELS, resolve) {
67
85
  const entry = stripClaudePrefix(model.trim().toLowerCase());
68
86
  if (entry === null)
69
87
  return null;
@@ -73,17 +91,38 @@ export function resolveClaudeServable(model, bases = BAKED_BASE_MODELS, resolve)
73
91
  // parses that prefix AFTER alias resolution; without the same pass here the
74
92
  // target reached the base check as the literal `claude:opus`, matched
75
93
  // nothing, and a perfectly valid fallback was skipped.
76
- const target = stripClaudePrefix((resolve ? resolve(entry) : (resolveAliasAgainst(entry, bases) ?? entry)).trim().toLowerCase());
77
- if (target === null)
94
+ const target = (resolve ? resolve(entry) : (resolveAliasAgainst(entry, bases) ?? entry))
95
+ .trim().toLowerCase();
96
+ const direct = servableTarget(target, bases);
97
+ if (direct)
98
+ return { model: direct };
99
+ // …and it may carry an effort suffix as well (`claude:opus:high`,
100
+ // `claude-opus-4-8-high`), which the request path strips on the Claude side
101
+ // exactly like this (dario#419, proxy.ts). Without the same pass the suffix
102
+ // survived into the base check, matched nothing, and the entry was silently
103
+ // unservable — the prefix fix in #1156 left this half unresolved. Tried only
104
+ // AFTER the name as written fails, so a real id that happens to end in an
105
+ // effort word keeps priority over the suffix reading.
106
+ //
107
+ // The effort is RETURNED, not discarded: an operator who writes `:high` on a
108
+ // chain entry is choosing the effort the failover request runs at, and the
109
+ // request path honours exactly that spelling for a client-sent model. Serving
110
+ // the failover at the pool default instead would silently answer a `high`
111
+ // request at whatever `--effort` happens to be (dario#1161).
112
+ const eff = parseEffortSuffix(target);
113
+ if (!eff.effort)
78
114
  return null;
79
- // A target that arrived prefixed still holds a shorthand (`claude:opus`
80
- // `opus`), so the catalog pass runs on it. Idempotent for a target already
81
- // canonical: resolveAliasAgainst only answers for family shorthands.
82
- const resolved = resolveAliasAgainst(target, bases) ?? target;
83
- const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
84
- return bases.some((b) => b.toLowerCase() === base) ? resolved : null;
115
+ const stripped = servableTarget(eff.model, bases);
116
+ return stripped ? { model: stripped, effort: eff.effort } : null;
117
+ }
118
+ /**
119
+ * The canonical id alone, for callers that only need to name the model. See
120
+ * resolveClaudeTarget for the effort half.
121
+ */
122
+ export function resolveClaudeServable(model, bases = BAKED_BASE_MODELS, resolve) {
123
+ return resolveClaudeTarget(model, bases, resolve)?.model ?? null;
85
124
  }
86
- /** Whether the Claude pool can serve `model`. See resolveClaudeServable. */
125
+ /** Whether the Claude pool can serve `model`. See resolveClaudeTarget. */
87
126
  export function isClaudeServableModel(model, bases = BAKED_BASE_MODELS, resolve) {
88
- return resolveClaudeServable(model, bases, resolve) !== null;
127
+ return resolveClaudeTarget(model, bases, resolve) !== null;
89
128
  }
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import type { IncomingMessage, ServerResponse } from 'node:http';
25
25
  import type { CodexAccountCredentials } from './codex-accounts.js';
26
- import { type ModelResolver } from './claude-model.js';
26
+ import { type ModelResolver, type ClaudeTarget } from './claude-model.js';
27
27
  export declare const CODEX_BACKEND_BASE_URL: string;
28
28
  /**
29
29
  * Client version sent on the model-discovery call. The backend REQUIRES the
@@ -87,6 +87,18 @@ export declare function isCodexModel(model: string, slugs: readonly string[]): b
87
87
  */
88
88
  export declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): string | null;
89
89
  export declare function pickClaudeFallback(models: readonly string[], slugs: readonly string[], bases?: readonly string[], resolve?: ModelResolver): string | null;
90
+ /**
91
+ * {@link pickClaudeFallback} with the effort the winning entry asked for.
92
+ *
93
+ * `claude:opus:high` names BOTH a model and an effort, and the request path
94
+ * honours exactly that spelling for a client-sent model. Selection kept only
95
+ * the model half, so a declined codex request configured that way reached the
96
+ * Claude pool at the pool default effort — the operator's `high` silently
97
+ * dropped (dario#1161). The caller applies `effort` when it rebuilds the
98
+ * Claude-bound request; `undefined` means the entry named no effort and the
99
+ * pool default stands.
100
+ */
101
+ export declare function pickClaudeTarget(models: readonly string[], slugs: readonly string[], bases?: readonly string[], resolve?: ModelResolver): ClaudeTarget | null;
90
102
  /**
91
103
  * Pull `chatgpt_account_id` out of the id_token's `https://api.openai.com/auth`
92
104
  * claim. Payload only — this is reading our own token for a routing header, not
@@ -1,5 +1,5 @@
1
1
  import { anthropicToResponsesRequest, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
2
- import { resolveClaudeServable } from './claude-model.js';
2
+ import { resolveClaudeTarget } from './claude-model.js';
3
3
  import { BAKED_BASE_MODELS } from './model-catalog.js';
4
4
  export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
5
5
  /** Originator string the codex CLI identifies itself with. */
@@ -133,12 +133,26 @@ export function pickClaudeFallback(models, slugs, bases = BAKED_BASE_MODELS, res
133
133
  // Returns the RESOLVED canonical id, not the entry as written: the caller
134
134
  // swaps it into the body after the proxy's own alias pass has already run,
135
135
  // so an alias returned raw would reach Anthropic unresolved and 400.
136
+ return pickClaudeTarget(models, slugs, bases, resolve)?.model ?? null;
137
+ }
138
+ /**
139
+ * {@link pickClaudeFallback} with the effort the winning entry asked for.
140
+ *
141
+ * `claude:opus:high` names BOTH a model and an effort, and the request path
142
+ * honours exactly that spelling for a client-sent model. Selection kept only
143
+ * the model half, so a declined codex request configured that way reached the
144
+ * Claude pool at the pool default effort — the operator's `high` silently
145
+ * dropped (dario#1161). The caller applies `effort` when it rebuilds the
146
+ * Claude-bound request; `undefined` means the entry named no effort and the
147
+ * pool default stands.
148
+ */
149
+ export function pickClaudeTarget(models, slugs, bases = BAKED_BASE_MODELS, resolve) {
136
150
  for (const m of models) {
137
151
  if (isCodexModel(m, slugs))
138
152
  continue;
139
- const resolved = resolveClaudeServable(m, bases, resolve);
140
- if (resolved)
141
- return resolved;
153
+ const target = resolveClaudeTarget(m, bases, resolve);
154
+ if (target)
155
+ return target;
142
156
  }
143
157
  return null;
144
158
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * effort.ts — the effort vocabulary and the model-name effort suffix.
3
+ *
4
+ * These lived in cc-template.ts, which is where the request path uses them.
5
+ * They moved down here because the pool-fallback classifier (claude-model.ts)
6
+ * has to parse the SAME suffix the request path parses, and that module is
7
+ * deliberately pure — it decides what the Claude pool can serve out of a base
8
+ * set and nothing else. Importing cc-template.js there would drag the CC
9
+ * template load (live capture / bundled snapshot, read at module init) into a
10
+ * classifier whose whole value is being decidable without one, and a second
11
+ * hand-written copy of the suffix list would be the other way to get it: two
12
+ * definitions of "what counts as an effort suffix" that drift apart silently,
13
+ * which is exactly the bug this file was extracted to fix (dario#1156 review).
14
+ *
15
+ * cc-template.ts re-exports all three, so every existing import site
16
+ * (proxy.ts, cli.ts, test/effort-flag.mjs) is unchanged.
17
+ */
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
+ export type EffortValue = 'low' | 'medium' | 'high' | 'xhigh' | 'ultracode' | 'max' | 'client';
20
+ export declare const VALID_EFFORT_VALUES: ReadonlyArray<EffortValue>;
21
+ export declare function parseEffortSuffix(model: string): {
22
+ model: string;
23
+ effort?: EffortValue;
24
+ };
package/dist/effort.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * effort.ts — the effort vocabulary and the model-name effort suffix.
3
+ *
4
+ * These lived in cc-template.ts, which is where the request path uses them.
5
+ * They moved down here because the pool-fallback classifier (claude-model.ts)
6
+ * has to parse the SAME suffix the request path parses, and that module is
7
+ * deliberately pure — it decides what the Claude pool can serve out of a base
8
+ * set and nothing else. Importing cc-template.js there would drag the CC
9
+ * template load (live capture / bundled snapshot, read at module init) into a
10
+ * classifier whose whole value is being decidable without one, and a second
11
+ * hand-written copy of the suffix list would be the other way to get it: two
12
+ * definitions of "what counts as an effort suffix" that drift apart silently,
13
+ * which is exactly the bug this file was extracted to fix (dario#1156 review).
14
+ *
15
+ * cc-template.ts re-exports all three, so every existing import site
16
+ * (proxy.ts, cli.ts, test/effort-flag.mjs) is unchanged.
17
+ */
18
+ export const VALID_EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'ultracode', 'max', 'client'];
19
+ /**
20
+ * dario#419 — strip an optional effort suffix off a model name, so OpenAI-compat
21
+ * clients that can't set `output_config.effort` (e.g. Cursor) can choose effort
22
+ * by model name: `opus-4-8:high` (colon) or Cursor-style `claude-opus-4-8-high`
23
+ * (hyphen). Only the wire-valid effort levels are recognized as a suffix — any
24
+ * other trailing token is left as part of the model name, and a bare model that
25
+ * IS an effort word (e.g. just "high") is left alone. Returns the model with the
26
+ * suffix removed plus the parsed effort (undefined when none). Exported for tests.
27
+ */
28
+ const SUFFIX_EFFORTS = ['ultracode', 'medium', 'xhigh', 'high', 'low', 'max'];
29
+ export function parseEffortSuffix(model) {
30
+ for (const e of SUFFIX_EFFORTS) {
31
+ for (const sep of [':', '-']) {
32
+ const tag = sep + e;
33
+ if (model.length > tag.length && model.endsWith(tag)) {
34
+ return { model: model.slice(0, -tag.length), effort: e };
35
+ }
36
+ }
37
+ }
38
+ return { model };
39
+ }
package/dist/proxy.js CHANGED
@@ -20,7 +20,7 @@ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncL
20
20
  import { handleAdminRequest } from './admin-api.js';
21
21
  import { createTokenBucket } from './rate-limit.js';
22
22
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
23
- import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeFallback, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
23
+ import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
24
24
  import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
25
25
  import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
26
26
  import { route as routeProvider } from './provider-adapter.js';
@@ -2746,8 +2746,10 @@ export async function startProxy(opts = {}) {
2746
2746
  // operator --model-alias first, then pinned + catalog aliases — so
2747
2747
  // `backup` or `opus48` in the chain works the way it does on a
2748
2748
  // request, and the classifier validates the id that would actually
2749
- // be forwarded.
2750
- const claudeTarget = pickClaudeFallback(poolFallbackModels, codexModels, getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m));
2749
+ // be forwarded. The entry's own effort suffix (`claude:opus:high`)
2750
+ // comes back with it see the swap below.
2751
+ const claudeTarget = pickClaudeTarget(poolFallbackModels, codexModels, getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m));
2752
+ const claudeTargetModel = claudeTarget?.model ?? null;
2751
2753
  const canDefer = claudeTarget !== null && pool.size > 0 && !upstreamApiKey;
2752
2754
  const codexReq = requestCount;
2753
2755
  const served = await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', fetch, canDefer,
@@ -2780,13 +2782,21 @@ export async function startProxy(opts = {}) {
2780
2782
  });
2781
2783
  if (served)
2782
2784
  return;
2783
- const swapped = buildPoolFallbackBody(body, claudeTarget);
2785
+ const swapped = buildPoolFallbackBody(body, claudeTargetModel);
2784
2786
  if (!swapped) {
2785
2787
  res.writeHead(503, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
2786
2788
  res.end(JSON.stringify({ error: { type: 'upstream_unavailable', message: 'Codex backend unavailable and the request body could not be re-pointed at the Claude pool.' } }));
2787
2789
  return;
2788
2790
  }
2789
- console.log(`[dario] #${requestCount} codex unavailable -> claude pool as ${claudeTarget}`);
2791
+ // An operator who writes `--pool-fallback=gpt-5.6-sol,claude:opus:high`
2792
+ // is choosing the effort the failover runs at, exactly as a client
2793
+ // naming `claude:opus:high` on the request is. Selection parsed the
2794
+ // suffix and then dropped it, so the rebuilt Claude request carried
2795
+ // the pool default instead (dario#1161). A chain entry with no
2796
+ // suffix leaves whatever the request itself asked for.
2797
+ if (claudeTarget?.effort)
2798
+ requestEffort = claudeTarget.effort;
2799
+ console.log(`[dario] #${requestCount} codex unavailable -> claude pool as ${claudeTargetModel}${claudeTarget?.effort ? ` (effort: ${claudeTarget.effort})` : ''}`);
2790
2800
  // Copy so the type matches the ArrayBuffer-backed buffer this
2791
2801
  // handler threads through (Buffer.concat's), as the other in-place
2792
2802
  // body rewrites above already do.
@@ -2797,7 +2807,7 @@ export async function startProxy(opts = {}) {
2797
2807
  // reads as a success, which is the exact bug class this release
2798
2808
  // spent its whole review budget hunting.
2799
2809
  parsedBody = null;
2800
- res.setHeader('x-dario-pool-fallback', claudeTarget);
2810
+ res.setHeader('x-dario-pool-fallback', claudeTargetModel);
2801
2811
  // fall through to Claude's turn below
2802
2812
  }
2803
2813
  if (rawModel && openaiBackend && decision.provider === 'openai') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.0.4",
3
+ "version": "6.0.5",
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": {