claudish 14.3.0 → 14.4.1

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/index.js CHANGED
@@ -575,7 +575,7 @@ var init_onepassword_config = __esm(() => {
575
575
  });
576
576
 
577
577
  // src/version.ts
578
- var VERSION = "14.3.0";
578
+ var VERSION = "14.4.1";
579
579
 
580
580
  // src/redact.ts
581
581
  function isPlaceholder(value) {
@@ -40453,6 +40453,20 @@ var init_claude_sse_sink = __esm(() => {
40453
40453
  init_redact();
40454
40454
  });
40455
40455
 
40456
+ // src/in-band-api-error.ts
40457
+ function formatInBandApiError(code, message) {
40458
+ return `
40459
+
40460
+ [API Error: ${code} ${message}]`;
40461
+ }
40462
+ function parseInBandApiError(answer, opts = {}) {
40463
+ const trimmed = answer.trim();
40464
+ const m = /^\[API Error: (\S+)([\s\S]*)\]$/.exec(trimmed) ?? (opts.trailing ? /\n\n\[API Error: (\S+)([^\n\]]*)\]$/.exec(trimmed) : null);
40465
+ if (!m)
40466
+ return null;
40467
+ return { code: m[1], text: `${m[1]}${m[2]}` };
40468
+ }
40469
+
40456
40470
  // src/handlers/shared/stream-parsers/responses/parallel-wrapper.ts
40457
40471
  function stripFunctionsPrefix(name) {
40458
40472
  return name.startsWith("functions.") ? name.slice("functions.".length) : name;
@@ -41161,13 +41175,9 @@ function sum(values) {
41161
41175
  function apiErrorText(code, message, opts) {
41162
41176
  const isCtxOverflow = code === "context_length_exceeded" || /context (length|window)|exceeds? the context|too long|maximum context/i.test(message);
41163
41177
  if (!isCtxOverflow)
41164
- return `
41165
-
41166
- [API Error: ${code} ${message}]`;
41178
+ return formatInBandApiError(code, message);
41167
41179
  const cap = opts.contextWindow && opts.contextWindow > 0 ? ` ~${Math.round(opts.contextWindow / 1000)}K tokens` : "";
41168
- return `
41169
-
41170
- [Context limit reached] This model's backend enforces a smaller context window${cap} than its API spec. ` + "Run /clear to start fresh (/compact will also fail \u2014 it re-sends the full conversation), " + `or route the model via \`oai@${opts.modelName}\` to use the full-size window.`;
41180
+ return formatInBandApiError(code, `Context limit reached. This model's backend enforces a smaller context window${cap} than its API spec. ` + "Run /clear to start fresh (/compact will also fail \u2014 it re-sends the full conversation), " + `or route the model via \`oai@${opts.modelName}\` to use the full-size window.`);
41171
41181
  }
41172
41182
  function stopReasonFor(incomplete, hasToolUse) {
41173
41183
  if (incomplete)
@@ -57687,6 +57697,10 @@ function findCatalogEntry(entries, model) {
57687
57697
  return loweredId;
57688
57698
  return entries.find((entry) => entry.aliases.some((alias) => alias.toLowerCase() === lowered));
57689
57699
  }
57700
+ function catalogModelId(model, cachePath) {
57701
+ const entries = cachePath ? readAllModelsCache(cachePath)?.entries : getCatalogEntries();
57702
+ return entries ? findCatalogEntry(entries, model)?.modelId : undefined;
57703
+ }
57690
57704
  function gatherFromConnections(entry, candidates, excluded, unmappedRoutes, cachePath) {
57691
57705
  for (const connection of entry.aggregators ?? []) {
57692
57706
  if (connection.routeStatus !== "mapped")
@@ -86620,7 +86634,9 @@ function classifyRunOutput(input) {
86620
86634
  return v;
86621
86635
  }
86622
86636
  function classifyTurnSignals(input) {
86623
- const { apiError, stopReason, promptRead } = input;
86637
+ const { stopReason, promptRead } = input;
86638
+ const inBand = input.apiError ? null : parseInBandApiError(input.answer, { trailing: promptRead?.complete === false });
86639
+ const apiError = input.apiError ?? (inBand ? { status: null, text: inBand.text } : null);
86624
86640
  if (apiError) {
86625
86641
  const status = apiError.status === null ? "" : ` (status ${apiError.status})`;
86626
86642
  return {
@@ -89693,15 +89709,15 @@ function stripUnrequestedProvider(spec, required, isCatalogModel = isCanonicalCa
89693
89709
  return spec;
89694
89710
  return spec.slice(spec.indexOf("@") + 1);
89695
89711
  }
89696
- function userRequiredProvidersArg(value, specs) {
89712
+ function userRequiredProvidersArg(value, specs, name = "user_required_provider") {
89697
89713
  if (value === undefined || value === null)
89698
89714
  return new Set;
89699
89715
  if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
89700
- throw new Error(`Invalid 'user_required_provider': expected an array of model specs.`);
89716
+ throw new Error(`Invalid '${name}': expected an array of model specs.`);
89701
89717
  }
89702
89718
  for (const spec of value) {
89703
89719
  if (!specs.includes(spec)) {
89704
- throw new Error(`Invalid 'user_required_provider': ${JSON.stringify(spec)} is not one of the ` + `models (${specs.join(", ")}). Entries must match a 'models' or 'judges' entry exactly.`);
89720
+ throw new Error(`Invalid '${name}': ${JSON.stringify(spec)} is not one of the ` + `models (${specs.join(", ")}). Entries must match a 'models' or 'judges' entry exactly.`);
89705
89721
  }
89706
89722
  }
89707
89723
  return new Set(value);
@@ -89729,6 +89745,132 @@ function strippedNote(stripped) {
89729
89745
  return;
89730
89746
  return `Provider removed so claudish routing decides: ${stripped.map((x) => `${x.from} -> ${x.to}`).join(", ")}. ` + "Pass 'user_required_provider' only when the user named the provider.";
89731
89747
  }
89748
+ function liveModelChoiceFacts(opts = {}) {
89749
+ const recommended = readLastRecommendedModels()?.models ?? null;
89750
+ if (!recommended || recommended.length === 0) {
89751
+ log("[mcp] model check skipped: no recommended models list on disk");
89752
+ }
89753
+ let rules;
89754
+ return {
89755
+ recommended: recommended && recommended.length > 0 ? recommended : null,
89756
+ userRule: (model) => {
89757
+ rules ??= loadRoutingRules();
89758
+ return matchRoutingRuleKey(model, rules) !== null;
89759
+ },
89760
+ inCatalog: (model) => !gatherRouteCandidates(model).catalogMiss,
89761
+ namespaceClaim: (model) => gatherRouteCandidates(model).candidates.length > 0,
89762
+ canonicalId: (model) => catalogModelId(model),
89763
+ catalogMissAllowed: (model) => {
89764
+ if (opts.projectRulesReadable === false)
89765
+ return true;
89766
+ if (!gatherRouteCandidates(model).catalogReadable)
89767
+ return true;
89768
+ const fallback = effectiveDefaultProvider();
89769
+ return fallback !== "" && fallback !== DEFAULT_FALLBACK_PROVIDER;
89770
+ }
89771
+ };
89772
+ }
89773
+ function familyOf(id) {
89774
+ return /^[a-z]+/i.exec(id)?.[0].toLowerCase() ?? id.toLowerCase();
89775
+ }
89776
+ function bareNameVerdict(spec, recommendedIds, facts) {
89777
+ if (parseModelChain(spec).length !== 1 || nativeRouteFor(spec) !== null)
89778
+ return;
89779
+ const parsed = parseModelSpec(spec);
89780
+ if (parsed.isExplicitProvider || parsed.isLegacySyntax)
89781
+ return;
89782
+ const model = parsed.model;
89783
+ if (recommendedIds.has(model) || facts.userRule(model))
89784
+ return;
89785
+ const canonical = facts.canonicalId?.(model);
89786
+ if (canonical !== undefined && recommendedIds.has(canonical))
89787
+ return;
89788
+ if (facts.inCatalog(model))
89789
+ return { kind: "not_recommended", model };
89790
+ if (facts.namespaceClaim(model) || facts.catalogMissAllowed?.(model))
89791
+ return;
89792
+ return { kind: "unknown_model", model };
89793
+ }
89794
+ function modelChoiceProblems(pairs, required, facts = liveModelChoiceFacts()) {
89795
+ const recommended = facts.recommended;
89796
+ if (!recommended)
89797
+ return [];
89798
+ const ids = new Set(recommended.map((m) => m.id));
89799
+ const problems = [];
89800
+ for (const { given, spec } of pairs) {
89801
+ if (required.has(given))
89802
+ continue;
89803
+ const verdict = bareNameVerdict(spec, ids, facts);
89804
+ if (!verdict)
89805
+ continue;
89806
+ const { kind, model } = verdict;
89807
+ const family = familyOf(model);
89808
+ const matching = [
89809
+ ...new Set(recommended.filter((m) => familyOf(m.id) === family || m.provider.toLowerCase() === model.toLowerCase()).sort(compareByReleaseDateDesc).map((m) => m.id))
89810
+ ];
89811
+ problems.push({ spec: given, kind, matching });
89812
+ }
89813
+ if (problems.length > 0) {
89814
+ log(`[mcp] model check: ${problems.map((p) => `${p.spec} ${p.kind}`).join(", ")}`);
89815
+ }
89816
+ return problems;
89817
+ }
89818
+ function recommendedChoices(recommended) {
89819
+ const byId = new Map;
89820
+ for (const m of recommended) {
89821
+ const row = byId.get(m.id);
89822
+ if (row) {
89823
+ row.tiers.push(m.recommendationTier);
89824
+ continue;
89825
+ }
89826
+ byId.set(m.id, {
89827
+ id: m.id,
89828
+ vendor: m.provider,
89829
+ tiers: [m.recommendationTier],
89830
+ ...m.releaseDate ? { released: m.releaseDate } : {}
89831
+ });
89832
+ }
89833
+ return [...byId.values()];
89834
+ }
89835
+ function recommendedSearchMatches(query, recommended) {
89836
+ const words = query.toLowerCase().split(/[\s,/]+/).filter((w) => w.length >= 2);
89837
+ if (words.length === 0)
89838
+ return [];
89839
+ const hits = recommended.filter((m) => {
89840
+ const id = m.id.toLowerCase();
89841
+ return words.some((w) => id.includes(w) || familyOf(m.id) === familyOf(w) || m.provider.toLowerCase() === w);
89842
+ });
89843
+ return recommendedChoices([...hits].sort(compareByReleaseDateDesc));
89844
+ }
89845
+ function modelChoiceResult(problems, recommended, how) {
89846
+ const unknown = problems.filter((p) => p.kind === "unknown_model");
89847
+ const old = problems.filter((p) => p.kind === "not_recommended");
89848
+ const lines = [
89849
+ `${unknown.length > 0 ? "unknown_model" : "not_recommended"}: nothing was started.`
89850
+ ];
89851
+ if (unknown.length > 0) {
89852
+ lines.push(`Not a model id: ${unknown.map((p) => JSON.stringify(p.spec)).join(", ")}. ` + "Pick the recommended model that best fits the task (for a family name, the newest " + "of the matching ids) and call again with its id.");
89853
+ }
89854
+ if (old.length > 0) {
89855
+ lines.push(`Not in the recommended list: ${old.map((p) => JSON.stringify(p.spec)).join(", ")}. ` + "Re-evaluate the choice against the recommended models below and call again with one " + `of them. If the user named this exact model, call again ${how}.`);
89856
+ }
89857
+ const matching = Object.fromEntries(problems.map((p) => [p.spec, p.matching]));
89858
+ lines.push(`Matching recommended ids: ${JSON.stringify(matching, null, 2)}`);
89859
+ const efforts = effortClarification([...new Set(problems.flatMap((p) => p.matching))]);
89860
+ if (efforts) {
89861
+ lines.push("Recommended effort for those ids, by task type (pass your task's level with the model " + `you pick): ${JSON.stringify(efforts, null, 2)}`);
89862
+ }
89863
+ lines.push(`Recommended models: ${JSON.stringify(recommendedChoices(recommended), null, 2)}`);
89864
+ return { content: [{ type: "text", text: lines.join(`
89865
+ `) }], isError: true };
89866
+ }
89867
+ function modelChoiceStop(pairs, required, how, opts = {}) {
89868
+ const facts = liveModelChoiceFacts(opts);
89869
+ const problems = modelChoiceProblems(pairs, required, facts);
89870
+ if (problems.length === 0 || !facts.recommended)
89871
+ return;
89872
+ return modelChoiceResult(problems, facts.recommended, how);
89873
+ }
89732
89874
  function buildChildClaudeFlags(agent, claudeFlags) {
89733
89875
  const extra = typeof claudeFlags === "string" ? claudeFlags.split(/\s+/).filter(Boolean) : [];
89734
89876
  const named = typeof agent === "string" ? agent.trim() : "";
@@ -89845,6 +89987,12 @@ function optionalString(raw, name) {
89845
89987
  throw new ContractErrorException("invalid_args", `'${name}' must be a non-empty string`);
89846
89988
  return raw;
89847
89989
  }
89990
+ function requiredString(raw, name) {
89991
+ const value = optionalString(raw, name);
89992
+ if (value === undefined)
89993
+ throw new ContractErrorException("invalid_args", `'${name}' is required`);
89994
+ return value;
89995
+ }
89848
89996
  function optionalBoolean(raw, name) {
89849
89997
  if (raw === undefined || raw === null)
89850
89998
  return false;
@@ -89953,6 +90101,7 @@ ${block.join(`
89953
90101
  type: "boolean",
89954
90102
  description: "True only when the USER named the provider ('use OpenRouter for it'): the " + "'provider@model' spec is then sent as given."
89955
90103
  },
90104
+ user_required_model: { type: "boolean", description: USER_REQUIRED_MODEL_DESCRIPTION },
89956
90105
  prompt: { type: "string", description: "The prompt to send to the model" },
89957
90106
  system_prompt: { type: "string", description: "Optional system prompt" },
89958
90107
  effort: {
@@ -89968,6 +90117,9 @@ ${block.join(`
89968
90117
  handler: async (args, ctx) => {
89969
90118
  try {
89970
90119
  const { specs, stripped } = strippedProviders([args.model], new Set(args.user_required_provider === true ? [args.model] : []));
90120
+ const choice = modelChoiceStop([{ given: args.model, spec: specs[0] }], new Set(args.user_required_model === true ? [args.model] : []), "with user_required_model: true");
90121
+ if (choice)
90122
+ return choice;
89971
90123
  const recommended = args.effort === undefined ? effortClarification([args.model]) : undefined;
89972
90124
  if (recommended) {
89973
90125
  return needsEffortResult(recommended, `with effort: "<level>" (effort: "${KEEP_EFFORT}" keeps Claude Code's own)`);
@@ -90065,6 +90217,26 @@ ${block.join(`
90065
90217
  return compareByReleaseDateDesc(orderingKey(a.model), orderingKey(b.model));
90066
90218
  }).slice(0, maxResults);
90067
90219
  const catalogMatches = searchCatalogModels(query, Math.max(maxResults, 5));
90220
+ const recommendedHits = recommendedSearchMatches(query, readLastRecommendedModels()?.models ?? []);
90221
+ const renderRecommendedSection = () => {
90222
+ if (recommendedHits.length === 0)
90223
+ return "";
90224
+ let s = `## Recommended models (pass one of these)
90225
+
90226
+ `;
90227
+ s += `| Bare name | Vendor | Tiers | Released |
90228
+ `;
90229
+ s += `|-----------|--------|-------|----------|
90230
+ `;
90231
+ for (const m of recommendedHits) {
90232
+ s += `| ${m.id} | ${m.vendor} | ${m.tiers.join(", ")} | ${m.released ?? "-"} |
90233
+ `;
90234
+ }
90235
+ return `${s}
90236
+ `;
90237
+ };
90238
+ const useWith = () => recommendedHits.length > 0 ? `
90239
+ Use with: run_prompt(model="${recommendedHits[0].id}", prompt="your prompt")` : "\nNo recommended model matches this query: call `list_models` for the recommended set.";
90068
90240
  const renderCatalogSection = () => {
90069
90241
  if (catalogMatches.length === 0)
90070
90242
  return "";
@@ -90081,12 +90253,17 @@ ${block.join(`
90081
90253
  s += `| ${m.modelId} | ${m.matchedAlias ?? "-"} | ${plans} |
90082
90254
  `;
90083
90255
  }
90084
- s += `
90085
- Pass the **bare name**. Routing puts a subscription ahead of the metered API and ` + "rewrites the model to that plan's wire id for you. An aggregator-qualified id " + "(`moonshotai/...`, `accounts/fireworks/...`) pins that aggregator and bills per token.\n";
90256
+ s += "\nOnly the recommended models above start without `user_required_model`. " + "Pass the **bare name**. Routing puts a subscription ahead of the metered API and " + "rewrites the model to that plan's wire id for you. An aggregator-qualified id " + "(`moonshotai/...`, `accounts/fireworks/...`) pins that aggregator and bills per token.\n";
90086
90257
  return s;
90087
90258
  };
90088
90259
  if (results.length === 0) {
90089
90260
  const catalog = renderCatalogSection();
90261
+ if (recommendedHits.length > 0) {
90262
+ const text = `No OpenRouter listing matches "${query}".
90263
+
90264
+ ${renderRecommendedSection()}${catalog}${useWith()}`;
90265
+ return { content: [{ type: "text", text }] };
90266
+ }
90090
90267
  const text = catalog ? `No OpenRouter listing matches "${query}", but claudish's catalog knows these:
90091
90268
  ${catalog}` : `No models found matching "${query}".
90092
90269
 
@@ -90096,6 +90273,7 @@ ${catalog}` : `No models found matching "${query}".
90096
90273
  let output = `# Search Results for "${query}"
90097
90274
 
90098
90275
  `;
90276
+ output += renderRecommendedSection();
90099
90277
  output += `## OpenRouter listing
90100
90278
 
90101
90279
  `;
@@ -90114,9 +90292,7 @@ ${catalog}` : `No models found matching "${query}".
90114
90292
  `;
90115
90293
  }
90116
90294
  output += renderCatalogSection();
90117
- const suggested = catalogMatches[0]?.modelId ?? results[0].model.id;
90118
- output += `
90119
- Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90295
+ output += useWith();
90120
90296
  return { content: [{ type: "text", text: output }] };
90121
90297
  }
90122
90298
  });
@@ -90300,6 +90476,11 @@ Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90300
90476
  items: { type: "string" },
90301
90477
  description: "The 'models' or 'judges' entries whose provider the USER named ('run Kimi on " + "OpenRouter'), exactly as written there. Every other 'provider@' on a catalog " + "model is removed so claudish routing picks the provider from the user's setup; " + "pass bare names unless the user asked for a provider."
90302
90478
  },
90479
+ user_required_model: {
90480
+ type: "array",
90481
+ items: { type: "string" },
90482
+ description: "The 'models' or 'judges' entries the USER named as exact models, exactly as " + "written there: they run even when not in the recommended list. Never list a " + "model you chose yourself."
90483
+ },
90303
90484
  input_file: {
90304
90485
  type: "string",
90305
90486
  description: "PREFERRED. Path to a file holding the task prompt, relative to the working " + "directory. Use this rather than `input` for anything longer than a sentence: " + "a prompt passed inline is echoed verbatim in the caller's terminal, where a " + "200-line review brief buries every other argument and makes the call " + "unreadable. Write the brief to the session directory first (input.md is the " + "conventional name) and point here."
@@ -90384,7 +90565,15 @@ Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90384
90565
  const existing = join56(resolved, "input.md");
90385
90566
  return existsSync37(existing) ? readFileSync37(existing, "utf-8") : undefined;
90386
90567
  };
90387
- if ((mode === "run" || mode === "run-and-judge") && givenModels && args.efforts === undefined) {
90568
+ const runsModels = mode === "run" || mode === "run-and-judge";
90569
+ const runsJudges = mode === "judge" || mode === "run-and-judge";
90570
+ const choice = modelChoiceStop([
90571
+ ...runsModels && givenModels && models ? givenModels.map((given, i) => ({ given, spec: models[i] })) : [],
90572
+ ...runsJudges && givenJudges && judges ? givenJudges.map((given, i) => ({ given, spec: judges[i] })) : []
90573
+ ], userRequiredProvidersArg(args.user_required_model, [...givenModels ?? [], ...givenJudges ?? []], "user_required_model"), 'with user_required_model: ["<that entry>"]');
90574
+ if (choice)
90575
+ return choice;
90576
+ if (runsModels && givenModels && args.efforts === undefined) {
90388
90577
  const recommended = effortClarification(givenModels);
90389
90578
  if (recommended) {
90390
90579
  return needsEffortResult(recommended, `with efforts: {"<model>": "<level>"} (efforts: {} keeps Claude Code's own for every model)`);
@@ -90673,6 +90862,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90673
90862
  type: "boolean",
90674
90863
  description: "True only when the USER named the provider: the 'provider@model' spec is then " + "used as given."
90675
90864
  },
90865
+ user_required_model: { type: "boolean", description: USER_REQUIRED_MODEL_DESCRIPTION },
90676
90866
  prompt: {
90677
90867
  type: "string",
90678
90868
  description: "Initial prompt to send. If omitted, send later via send_input."
@@ -90720,6 +90910,11 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90720
90910
  await assertMagmuxAvailable();
90721
90911
  const { specs, stripped } = strippedProviders([args.model], new Set(args.user_required_provider === true ? [args.model] : []));
90722
90912
  const requestedModel = specs[0];
90913
+ const choice = modelChoiceStop([{ given: args.model, spec: requestedModel }], new Set(args.user_required_model === true ? [args.model] : []), "with user_required_model: true", {
90914
+ projectRulesReadable: typeof args.work_dir !== "string" || resolve8(args.work_dir) === process.cwd()
90915
+ });
90916
+ if (choice)
90917
+ return choice;
90723
90918
  const recommended = args.effort === undefined ? effortClarification([args.model]) : undefined;
90724
90919
  if (recommended) {
90725
90920
  return needsEffortResult(recommended, `with effort: "<level>" (effort: "${KEEP_EFFORT}" keeps Claude Code's own)`);
@@ -90787,7 +90982,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90787
90982
  },
90788
90983
  group: "session",
90789
90984
  handler: async (args) => {
90790
- const result = sessionManager.sendInput(args.session_id, args.text);
90985
+ const result = sessionManager.sendInput(requiredString(args.session_id, "session_id"), requiredString(args.text, "text"));
90791
90986
  return {
90792
90987
  content: [{ type: "text", text: JSON.stringify(result) }]
90793
90988
  };
@@ -90810,7 +91005,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90810
91005
  group: "session",
90811
91006
  handler: async (args) => {
90812
91007
  try {
90813
- const output = sessionManager.getOutput(args.session_id, args.tail_lines);
91008
+ const output = sessionManager.getOutput(requiredString(args.session_id, "session_id"), args.tail_lines);
90814
91009
  return {
90815
91010
  content: [{ type: "text", text: JSON.stringify(output) }]
90816
91011
  };
@@ -90891,7 +91086,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90891
91086
  group: "session",
90892
91087
  handler: async (args) => {
90893
91088
  try {
90894
- const diagnostics = sessionManager.getDiagnostics(args.session_id, args.event_limit);
91089
+ const diagnostics = sessionManager.getDiagnostics(requiredString(args.session_id, "session_id"), args.event_limit);
90895
91090
  return {
90896
91091
  content: [{ type: "text", text: JSON.stringify(diagnostics) }]
90897
91092
  };
@@ -91114,7 +91309,7 @@ Each session is an interactive Claude Code running the model in its own headless
91114
91309
  5. FAILED or TIMEOUT \u2014 or COMPLETED with an empty or surprising answer \u2014 call get_diagnostics. It returns the child's final screen, the upstream error bodies, the recent state records, the resolved model chain and the paths to the full records. It needs no re-run and no debug flag.
91115
91310
  6. capture_session reads a session's current screen; resize_session sets its terminal size (any time, mid-turn included); cancel_session stops it. Nothing stops a session for being idle: a child inside a long build is silent and working.
91116
91311
 
91117
- The session_id from create_session is the key for every session tool.`, proxyInstance = null, proxyStarting = null, SCORE_TIE_EPSILON = 0.000001, NEXT_STEP, KEEP_EFFORT = "default", sanitize2, TOOL_MODES;
91312
+ The session_id from create_session is the key for every session tool.`, proxyInstance = null, proxyStarting = null, SCORE_TIE_EPSILON = 0.000001, NEXT_STEP, KEEP_EFFORT = "default", USER_REQUIRED_MODEL_DESCRIPTION, sanitize2, TOOL_MODES;
91118
91313
  var init_mcp_server = __esm(() => {
91119
91314
  init_server2();
91120
91315
  init_stdio2();
@@ -91146,6 +91341,8 @@ var init_mcp_server = __esm(() => {
91146
91341
  init_preflight_chain();
91147
91342
  init_probe_live();
91148
91343
  init_provider_definitions();
91344
+ init_route_candidates();
91345
+ init_routing_rules();
91149
91346
  init_proxy_server();
91150
91347
  init_recommended_listing();
91151
91348
  init_redact();
@@ -91172,6 +91369,7 @@ var init_mcp_server = __esm(() => {
91172
91369
  empty_output: "retry once; if it repeats, drop the model",
91173
91370
  shape_mismatch: "the answer (assistant text after the task was read) did not match. Every assistant " + "message of the turn was read from the transcript, so nothing was lost in transit \u2014 " + "the model did not produce it. Re-prompt with the required format restated; do NOT " + "count this slot as a vote"
91174
91371
  };
91372
+ USER_REQUIRED_MODEL_DESCRIPTION = "True only when the USER named this exact model: it then runs even when it is not in the " + "recommended list. Never set it for a model you chose yourself.";
91175
91373
  sanitize2 = sanitizeForReport;
91176
91374
  TOOL_MODES = ["all", "low-level", "agentic", "session", "debug"];
91177
91375
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "14.3.0",
3
+ "version": "14.4.1",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,10 +71,10 @@
71
71
  "ai"
72
72
  ],
73
73
  "optionalDependencies": {
74
- "@claudish/magmux-darwin-arm64": "14.3.0",
75
- "@claudish/magmux-darwin-x64": "14.3.0",
76
- "@claudish/magmux-linux-arm64": "14.3.0",
77
- "@claudish/magmux-linux-x64": "14.3.0"
74
+ "@claudish/magmux-darwin-arm64": "14.4.1",
75
+ "@claudish/magmux-darwin-x64": "14.4.1",
76
+ "@claudish/magmux-linux-arm64": "14.4.1",
77
+ "@claudish/magmux-linux-x64": "14.4.1"
78
78
  },
79
79
  "author": "Jack Rudenko <i@madappgang.com>",
80
80
  "license": "MIT",
@@ -58,9 +58,13 @@ subscriptions first, their preferences and manual overrides. Do not choose it yo
58
58
  `create_session` / `run_prompt`: `true`).
59
59
  3. **User named exact models:** pass them as given. No `preflight` or `search_models`
60
60
  call is needed to check them: claudish routes and checks them. You still read
61
- `list_models` once for their efforts (next section).
61
+ `list_models` once for their efforts (next section). A named model outside the
62
+ recommended list answers `not_recommended`; list it in `user_required_model` (`team`:
63
+ the entries; `create_session` / `run_prompt`: `true`) and call again.
62
64
  4. **User named only a family or vendor** ("a GPT model", "minimax", "latest Grok"): call
63
- `list_models` and take the newest model of that family from it, as a bare name.
65
+ `list_models` and take the newest model of that family from it, as a bare name. A
66
+ family name (`gpt`) or a model you recall (`gpt-5`) answers `unknown_model` /
67
+ `not_recommended` with the recommended ids: pick from them and call again.
64
68
 
65
69
  ### 🎚️ Reasoning Effort per Model
66
70