claudish 9.2.1 → 9.3.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.
Files changed (2) hide show
  1. package/dist/index.js +1925 -480
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -715,7 +715,7 @@ var init_onepassword_config = __esm(() => {
715
715
  });
716
716
 
717
717
  // src/version.ts
718
- var VERSION = "9.2.1";
718
+ var VERSION = "9.3.1";
719
719
 
720
720
  // src/logger.ts
721
721
  import { appendFile, existsSync as existsSync2, mkdirSync, readdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
@@ -20992,6 +20992,11 @@ function messageStartUsage(priorInputTokens) {
20992
20992
  }
20993
20993
 
20994
20994
  // src/handlers/shared/stream-parsers/openai-sse.ts
20995
+ function formatRawSseLogPayload(dataStr) {
20996
+ if (dataStr.length <= SSE_LOG_MAX_CHARS)
20997
+ return dataStr;
20998
+ return `${dataStr.substring(0, SSE_LOG_MAX_CHARS)} ${SSE_LOG_TRUNCATION_MARKER} original_chars=${dataStr.length}`;
20999
+ }
20995
21000
  function validateToolArguments(toolName, argsStr, toolSchemas, textContent) {
20996
21001
  const result = validateAndRepairToolCall(toolName, argsStr, toolSchemas, textContent);
20997
21002
  if (result.repaired) {
@@ -21253,7 +21258,7 @@ data: ${JSON.stringify(d)}
21253
21258
  if (!line.trim() || !line.startsWith("data: "))
21254
21259
  continue;
21255
21260
  const dataStr = line.slice(6);
21256
- log(`[SSE:openai] ${dataStr.substring(0, 300)}`);
21261
+ log(`[SSE:openai] ${formatRawSseLogPayload(dataStr)}`);
21257
21262
  if (dataStr === "[DONE]") {
21258
21263
  await finalize("done");
21259
21264
  return;
@@ -21539,6 +21544,7 @@ data: ${JSON.stringify(d)}
21539
21544
  }
21540
21545
  });
21541
21546
  }
21547
+ var SSE_LOG_MAX_CHARS = 1e6, SSE_LOG_TRUNCATION_MARKER = "<<<CLAUDISH_SSE_TRUNCATED>>>";
21542
21548
  var init_openai_sse = __esm(() => {
21543
21549
  init_logger();
21544
21550
  init_tool_call_recovery();
@@ -33657,7 +33663,7 @@ data: ${JSON.stringify(data)}
33657
33663
  if (data === "[DONE]")
33658
33664
  continue;
33659
33665
  if (getLogLevel() === "debug") {
33660
- log(`[SSE:responses] ${data.substring(0, 300)}`);
33666
+ log(`[SSE:responses] ${formatRawSseLogPayload(data)}`);
33661
33667
  }
33662
33668
  try {
33663
33669
  const event = JSON.parse(data);
@@ -33921,6 +33927,7 @@ var init_openai_responses_sse = __esm(() => {
33921
33927
  init_reasoning_cache();
33922
33928
  init_logger();
33923
33929
  init_anthropic_error();
33930
+ init_openai_sse();
33924
33931
  });
33925
33932
 
33926
33933
  // src/handlers/shared/token-tracker.ts
@@ -34438,7 +34445,7 @@ class ComposedHandler {
34438
34445
  requestPayload = this.provider.transformPayload(requestPayload, claudeRequest);
34439
34446
  }
34440
34447
  const endpoint = this.provider.getEndpoint(this.targetModel);
34441
- const headers = await this.provider.getHeaders();
34448
+ const headers = await this.provider.getHeaders(payload);
34442
34449
  const serialized = this.provider.serializeBody?.(requestPayload);
34443
34450
  headers["Content-Type"] = serialized?.contentType ?? "application/json";
34444
34451
  log(`[${this.provider.displayName}] Calling API: ${endpoint}`);
@@ -34508,7 +34515,7 @@ class ComposedHandler {
34508
34515
  log(`[${this.provider.displayName}] Parameter rejected \u2014 retrying: ${recovery.note}`);
34509
34516
  requestPayload = recovery.payload;
34510
34517
  const retrySerialized = this.provider.serializeBody?.(requestPayload);
34511
- const retryHeaders = await this.provider.getHeaders();
34518
+ const retryHeaders = await this.provider.getHeaders(payload);
34512
34519
  retryHeaders["Content-Type"] = retrySerialized?.contentType ?? "application/json";
34513
34520
  const retryResp = await fetch(endpoint, {
34514
34521
  method: "POST",
@@ -34529,7 +34536,7 @@ class ComposedHandler {
34529
34536
  log(`[${this.provider.displayName}] Got 401, forcing auth refresh and retrying`);
34530
34537
  try {
34531
34538
  await this.provider.forceRefreshAuth();
34532
- const retryHeaders = await this.provider.getHeaders();
34539
+ const retryHeaders = await this.provider.getHeaders(payload);
34533
34540
  retryHeaders["Content-Type"] = serialized?.contentType ?? "application/json";
34534
34541
  const retryInit = this.provider.getRequestInit?.() || {};
34535
34542
  const retryResp = await fetch(endpoint, {
@@ -35688,21 +35695,31 @@ var init_ollamacloud = __esm(() => {
35688
35695
  init_probe_discovery();
35689
35696
  });
35690
35697
 
35691
- // src/providers/transport/openai-codex.ts
35698
+ // src/providers/transport/conversation-key.ts
35692
35699
  import { createHash as createHash5, randomBytes as randomBytes6 } from "crypto";
35693
- var FALLBACK_CONVERSATION_KEY, OpenAICodexTransport;
35700
+ function conversationKey(claudeRequest) {
35701
+ const sessionId = extractSessionId(claudeRequest);
35702
+ if (!sessionId)
35703
+ return `claudish_${FALLBACK_CONVERSATION_KEY}`;
35704
+ return `claudish_${createHash5("sha256").update(sessionId).digest("hex").slice(0, 32)}`;
35705
+ }
35706
+ var FALLBACK_CONVERSATION_KEY;
35707
+ var init_conversation_key = __esm(() => {
35708
+ init_harness();
35709
+ FALLBACK_CONVERSATION_KEY = randomBytes6(16).toString("hex");
35710
+ });
35711
+
35712
+ // src/providers/transport/openai-codex.ts
35713
+ var OpenAICodexTransport;
35694
35714
  var init_openai_codex = __esm(() => {
35695
35715
  init_codex_api_format();
35696
35716
  init_model_catalog();
35697
35717
  init_authority();
35698
35718
  init_billing_probe();
35699
- init_harness();
35719
+ init_conversation_key();
35700
35720
  init_openai();
35701
- FALLBACK_CONVERSATION_KEY = randomBytes6(16).toString("hex");
35702
35721
  OpenAICodexTransport = class OpenAICodexTransport extends OpenAIProviderTransport {
35703
35722
  cachedAuth = null;
35704
- cachedCacheKeyFor;
35705
- cachedCacheKey = "";
35706
35723
  async refreshAuth() {
35707
35724
  try {
35708
35725
  this.cachedAuth = await credentials.getRequestAuth("openai-codex", { model: "" });
@@ -35737,14 +35754,7 @@ var init_openai_codex = __esm(() => {
35737
35754
  return this.cachedAuth?.transformPayload?.(normalizedPayload) ?? normalizedPayload;
35738
35755
  }
35739
35756
  resolvePromptCacheKey(claudeRequest) {
35740
- const sessionId = extractSessionId(claudeRequest);
35741
- if (!sessionId)
35742
- return `claudish_${FALLBACK_CONVERSATION_KEY}`;
35743
- if (this.cachedCacheKeyFor !== sessionId) {
35744
- this.cachedCacheKeyFor = sessionId;
35745
- this.cachedCacheKey = `claudish_${createHash5("sha256").update(sessionId).digest("hex").slice(0, 32)}`;
35746
- }
35747
- return this.cachedCacheKey;
35757
+ return conversationKey(claudeRequest);
35748
35758
  }
35749
35759
  getContextWindow() {
35750
35760
  return lookupModelForProvider(this.modelName, this.name) ?? 0;
@@ -35752,6 +35762,21 @@ var init_openai_codex = __esm(() => {
35752
35762
  };
35753
35763
  });
35754
35764
 
35765
+ // src/providers/transport/opencode-zen.ts
35766
+ var OpenCodeZenTransport;
35767
+ var init_opencode_zen = __esm(() => {
35768
+ init_conversation_key();
35769
+ init_openai();
35770
+ OpenCodeZenTransport = class OpenCodeZenTransport extends OpenAIProviderTransport {
35771
+ async getHeaders(claudeRequest) {
35772
+ return {
35773
+ ...await super.getHeaders(),
35774
+ "x-opencode-session": conversationKey(claudeRequest)
35775
+ };
35776
+ }
35777
+ };
35778
+ });
35779
+
35755
35780
  // src/providers/transport/vertex-oauth.ts
35756
35781
  function parseVertexModel(modelId) {
35757
35782
  const parts = modelId.split("/");
@@ -35892,6 +35917,7 @@ var init_provider_profiles = __esm(() => {
35892
35917
  init_ollamacloud();
35893
35918
  init_openai_codex();
35894
35919
  init_openai();
35920
+ init_opencode_zen();
35895
35921
  init_vertex_oauth();
35896
35922
  geminiProfile = {
35897
35923
  createHandler(ctx) {
@@ -36013,7 +36039,7 @@ var init_provider_profiles = __esm(() => {
36013
36039
  const isGoProvider = ctx.provider.name === "opencode-zen-go";
36014
36040
  if (ctx.modelName.toLowerCase().startsWith("gpt-")) {
36015
36041
  const responsesProvider = { ...ctx.provider, apiPath: "/v1/responses" };
36016
- const transport = new OpenAIProviderTransport(responsesProvider, ctx.modelName, zenApiKey);
36042
+ const transport = new OpenCodeZenTransport(responsesProvider, ctx.modelName, zenApiKey);
36017
36043
  const adapter = new CodexAPIFormat(ctx.modelName);
36018
36044
  const handler = new ComposedHandler(transport, ctx.targetModel, ctx.modelName, ctx.port, {
36019
36045
  adapter,
@@ -36023,7 +36049,7 @@ var init_provider_profiles = __esm(() => {
36023
36049
  log(`[Proxy] Created OpenCode Zen${isGoProvider ? " Go" : ""} (Responses API composed): ${ctx.modelName}`);
36024
36050
  return handler;
36025
36051
  }
36026
- const transport = new OpenAIProviderTransport(ctx.provider, ctx.modelName, zenApiKey);
36052
+ const transport = new OpenCodeZenTransport(ctx.provider, ctx.modelName, zenApiKey);
36027
36053
  const adapter = new OpenAIAPIFormat(ctx.modelName);
36028
36054
  const handler = new ComposedHandler(transport, ctx.targetModel, ctx.modelName, ctx.port, {
36029
36055
  adapter,
@@ -40387,6 +40413,8 @@ function teamSlotIdleSeconds(teamSessionId) {
40387
40413
  return null;
40388
40414
  const out = {};
40389
40415
  for (const slotId of run.processes.keys()) {
40416
+ if (!run.isRunning(slotId))
40417
+ continue;
40390
40418
  const idle = run.idleMsFor(slotId);
40391
40419
  if (idle !== null)
40392
40420
  out[slotId] = Math.round(idle / 1000);
@@ -40399,6 +40427,8 @@ function teamSlotActivity(teamSessionId) {
40399
40427
  return null;
40400
40428
  const out = {};
40401
40429
  for (const slotId of run.processes.keys()) {
40430
+ if (!run.isRunning(slotId))
40431
+ continue;
40402
40432
  const activity = run.activityFor(slotId);
40403
40433
  if (activity !== null)
40404
40434
  out[slotId] = activity;
@@ -40411,6 +40441,8 @@ function teamSlotLiveBytes(teamSessionId) {
40411
40441
  return null;
40412
40442
  const out = {};
40413
40443
  for (const slotId of run.processes.keys()) {
40444
+ if (!run.isRunning(slotId))
40445
+ continue;
40414
40446
  const bytes = run.liveBytesFor(slotId);
40415
40447
  if (bytes !== null)
40416
40448
  out[slotId] = bytes;
@@ -40723,7 +40755,6 @@ async function startModels(sessionPath, opts = {}) {
40723
40755
  return;
40724
40756
  captureFinalized = true;
40725
40757
  absorb(slotReducer.end());
40726
- slotReducer.dispose();
40727
40758
  outputStream.end();
40728
40759
  };
40729
40760
  proc.stdout?.on("end", finalizeCapture);
@@ -40755,12 +40786,21 @@ async function startModels(sessionPath, opts = {}) {
40755
40786
  const completionPromise = new Promise((resolve) => {
40756
40787
  let exitCode = null;
40757
40788
  let resolved = false;
40789
+ const settleReducer = () => {
40790
+ if (!reducer)
40791
+ return;
40792
+ const state = statusCache.models[anonId]?.state;
40793
+ const failed = state === "FAILED" || state === "EMPTY";
40794
+ reducer.settle(failed && cancelledSlots.has(anonId) ? "cancelled" : state === "COMPLETED" ? "completed" : state === "TIMEOUT" ? "timeout" : "failed");
40795
+ reducer.dispose();
40796
+ };
40758
40797
  const finish = () => {
40759
40798
  if (resolved)
40760
40799
  return;
40761
40800
  if (statusCache.models[anonId].state === "TIMEOUT") {
40762
40801
  resolved = true;
40763
40802
  reconcileTimedOutOutput(anonId, byteCount, stdoutTail, stderr);
40803
+ settleReducer();
40764
40804
  resolve();
40765
40805
  return;
40766
40806
  }
@@ -40815,6 +40855,7 @@ async function startModels(sessionPath, opts = {}) {
40815
40855
  error: undefined
40816
40856
  });
40817
40857
  }
40858
+ settleReducer();
40818
40859
  opts.onStatusChange?.(anonId, statusCache.models[anonId]);
40819
40860
  resolve();
40820
40861
  };
@@ -40870,6 +40911,7 @@ async function startModels(sessionPath, opts = {}) {
40870
40911
  idleMsFor: (slotId) => runtimes.get(slotId)?.getIdleMs() ?? null,
40871
40912
  activityFor: (slotId) => runtimes.get(slotId)?.getActivity() ?? null,
40872
40913
  liveBytesFor: (slotId) => runtimes.get(slotId)?.getByteCount() ?? null,
40914
+ isRunning: (slotId) => statusCache.models[slotId]?.state === "RUNNING",
40873
40915
  cancelledSlots
40874
40916
  });
40875
40917
  const done = (async () => {
@@ -44812,166 +44854,6 @@ ${text}`;
44812
44854
  };
44813
44855
  });
44814
44856
 
44815
- // src/handlers/fallback-handler.ts
44816
- class FallbackHandler {
44817
- candidates;
44818
- lastSuccessIndex = 0;
44819
- constructor(candidates) {
44820
- this.candidates = candidates;
44821
- }
44822
- async handle(c, payload) {
44823
- const errors = [];
44824
- const startIndex = this.lastSuccessIndex;
44825
- for (let attempt = 0;attempt < this.candidates.length; attempt++) {
44826
- const idx = (startIndex + attempt) % this.candidates.length;
44827
- const { name, handler } = this.candidates[idx];
44828
- const isLast = attempt === this.candidates.length - 1;
44829
- try {
44830
- if (errors.length > 0 && handler instanceof ComposedHandler) {
44831
- try {
44832
- handler.setFallbackMeta(this.candidates.map((c) => c.name), errors.length);
44833
- } catch {}
44834
- }
44835
- const response = await handler.handle(c, payload);
44836
- if (response.ok) {
44837
- this.lastSuccessIndex = idx;
44838
- if (errors.length > 0) {
44839
- logStderr(`[Fallback] ${name} succeeded after ${errors.length} failed attempt(s)`);
44840
- if (handler instanceof ComposedHandler) {
44841
- handler.getTokenTracker()?.setProviderDisplayName(name);
44842
- }
44843
- }
44844
- return response;
44845
- }
44846
- const errorBody = await response.clone().text();
44847
- if (!isRetryableError(response.status, errorBody, name)) {
44848
- if (errors.length > 0) {
44849
- errors.push({ provider: name, status: response.status, message: errorBody });
44850
- return this.formatCombinedError(c, errors, payload.model);
44851
- }
44852
- return response;
44853
- }
44854
- errors.push({ provider: name, status: response.status, message: errorBody });
44855
- if (!isLast) {
44856
- if (hasQuotaExhaustionWording(errorBody)) {
44857
- logStderr(`[Fallback] ${name} subscription allowance is spent \u2014 falling through to the next provider, which is billed PER TOKEN. Use a provider prefix (e.g. \`zgo@model\`) to fail instead of switching.`);
44858
- } else {
44859
- logStderr(`[Fallback] ${name} failed (HTTP ${response.status}), trying next provider...`);
44860
- }
44861
- }
44862
- } catch (err) {
44863
- errors.push({ provider: name, status: 0, message: err.message });
44864
- if (!isLast) {
44865
- logStderr(`[Fallback] ${name} error: ${err.message}, trying next provider...`);
44866
- }
44867
- }
44868
- }
44869
- return this.formatCombinedError(c, errors, payload.model);
44870
- }
44871
- formatCombinedError(c, errors, modelName) {
44872
- const summary = errors.map((e) => ` ${e.provider}: HTTP ${e.status || "ERR"} \u2014 ${truncate(parseErrorMessage(e.message), 150)}`).join(`
44873
- `);
44874
- logStderr(`[Fallback] All ${errors.length} provider(s) failed for ${modelName || "model"}:
44875
- ${summary}`);
44876
- return c.json({
44877
- error: {
44878
- type: "all_providers_failed",
44879
- message: `All ${errors.length} providers failed for model '${modelName || "unknown"}'`,
44880
- attempts: errors.map((e) => ({
44881
- provider: e.provider,
44882
- status: e.status,
44883
- error: truncate(parseErrorMessage(e.message), 200)
44884
- }))
44885
- }
44886
- }, exhaustedChainStatus(errors));
44887
- }
44888
- async shutdown() {
44889
- for (const { handler } of this.candidates) {
44890
- if (typeof handler.shutdown === "function") {
44891
- await handler.shutdown();
44892
- }
44893
- }
44894
- }
44895
- }
44896
- function isRetryableError(status, errorBody, provider) {
44897
- if (hasQuotaExhaustionWording(errorBody))
44898
- return true;
44899
- const upstream = status === 400 ? extractUpstreamStatus(errorBody) : undefined;
44900
- if (upstream === 401 || upstream === 403 || upstream === 402 || upstream === 429) {
44901
- return true;
44902
- }
44903
- if (status === 401 || status === 403)
44904
- return true;
44905
- if (status === 402)
44906
- return true;
44907
- if (status === 404)
44908
- return true;
44909
- if (status === 429)
44910
- return true;
44911
- const lower = errorBody.toLowerCase();
44912
- if (status === 422) {
44913
- if (lower.includes("not available") || lower.includes("model not found") || lower.includes("not supported")) {
44914
- return true;
44915
- }
44916
- }
44917
- if (status === 400) {
44918
- if (lower.includes("model not found") || lower.includes("not registered") || lower.includes("does not exist") || lower.includes("unknown model") || lower.includes("unsupported model") || lower.includes("no healthy deployment") || lower.includes("requires a google cloud project") || lower.includes("unsupported_client")) {
44919
- return true;
44920
- }
44921
- if (provider?.toLowerCase().includes("antigravity") && lower.includes("invalid argument")) {
44922
- return true;
44923
- }
44924
- if (isProvider(provider, "opencodezen") && (lower.includes("upstream request failed") || lower.includes("error from provider ("))) {
44925
- return true;
44926
- }
44927
- }
44928
- if (status === 500) {
44929
- if (lower.includes("insufficient balance") || lower.includes("insufficient credit") || lower.includes("quota exceeded") || lower.includes("billing")) {
44930
- return true;
44931
- }
44932
- }
44933
- return false;
44934
- }
44935
- function exhaustedChainStatus(errors) {
44936
- if (errors.length === 0)
44937
- return 400;
44938
- const isTransient = (e) => {
44939
- if (e.status === 429 || e.status === 503)
44940
- return true;
44941
- if (hasQuotaExhaustionWording(e.message))
44942
- return true;
44943
- const upstream = e.status === 400 ? extractUpstreamStatus(e.message) : undefined;
44944
- return upstream === 429 || upstream === 503;
44945
- };
44946
- return errors.every(isTransient) ? 503 : 400;
44947
- }
44948
- function isProvider(provider, needle) {
44949
- if (!provider)
44950
- return false;
44951
- return provider.toLowerCase().replace(/[^a-z0-9]/g, "").includes(needle);
44952
- }
44953
- function parseErrorMessage(body) {
44954
- try {
44955
- const parsed = JSON.parse(body);
44956
- if (typeof parsed.error === "string")
44957
- return parsed.error;
44958
- if (typeof parsed.error?.message === "string")
44959
- return parsed.error.message;
44960
- if (typeof parsed.message === "string")
44961
- return parsed.message;
44962
- } catch {}
44963
- return body;
44964
- }
44965
- function truncate(s, max) {
44966
- return s.length > max ? `${s.slice(0, max)}...` : s;
44967
- }
44968
- var init_fallback_handler = __esm(() => {
44969
- init_logger();
44970
- init_composed_handler();
44971
- init_anthropic_error();
44972
- init_quota_exhaustion();
44973
- });
44974
-
44975
44857
  // src/handlers/native-handler-advisor.ts
44976
44858
  import { appendFileSync as appendFileSync7 } from "fs";
44977
44859
  function loadAdvisorSwapConfig(cliModels, cliCollector) {
@@ -45022,17 +44904,65 @@ function stripAdvisorBeta(betaHeader) {
45022
44904
  changed: true
45023
44905
  };
45024
44906
  }
44907
+ function scrubSecrets(text) {
44908
+ return text.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, `Bearer ${REDACTED}`).replace(/\bsk-[A-Za-z0-9_-]{8,}/g, REDACTED).replace(/\bAIza[0-9A-Za-z_-]{20,}/g, REDACTED).replace(/\bxai-[A-Za-z0-9_-]{16,}/g, REDACTED);
44909
+ }
44910
+ function sanitizeAdvisorReason(text, secretValues = []) {
44911
+ let out = text;
44912
+ for (const secret of secretValues) {
44913
+ const value = secret?.trim();
44914
+ if (!value || value.length < MIN_REDACTABLE_SECRET_LENGTH)
44915
+ continue;
44916
+ out = out.split(value).join(REDACTED);
44917
+ }
44918
+ return scrubSecrets(out);
44919
+ }
44920
+ function credentialValuesInHeaders(headers) {
44921
+ const values = [];
44922
+ const bearer = headers.Authorization ?? headers.authorization;
44923
+ if (bearer)
44924
+ values.push(bearer.replace(/^Bearer\s+/i, "").trim());
44925
+ const apiKey = headers["x-api-key"];
44926
+ if (apiKey)
44927
+ values.push(apiKey.trim());
44928
+ return values;
44929
+ }
44930
+ function credentialValuesOf(apiKeys) {
44931
+ return Object.values(apiKeys).filter((v) => typeof v === "string");
44932
+ }
45025
44933
  function logAdvisorEvent(cfg, event) {
44934
+ const ts = new Date().toISOString();
44935
+ const record = { ts, ...event };
44936
+ let line;
44937
+ try {
44938
+ line = scrubSecrets(JSON.stringify(record));
44939
+ } catch {
44940
+ return;
44941
+ }
44942
+ if (typeof event.kind === "string" && ORIGIN_RECORD_KINDS.has(event.kind)) {
44943
+ if (getLogFilePath() !== null)
44944
+ log(`${ADVISOR_ORIGIN_LOG_PREFIX} ${line}`);
44945
+ appendLine(getAlwaysOnLogPath(), `[${ts}] ${ADVISOR_ORIGIN_LOG_PREFIX} ${line}`);
44946
+ }
44947
+ appendLine(cfg.logPath, line);
44948
+ }
44949
+ function appendLine(path, line) {
44950
+ if (!path)
44951
+ return;
44952
+ try {
44953
+ appendFileSync7(path, `${line}
44954
+ `);
44955
+ } catch {}
44956
+ }
44957
+ function recordAdvisorEventsFromResponseBody(cfg, body, sessionId) {
44958
+ collectAdvisorIdsFromValue(body, 0, sessionId);
45026
44959
  if (!cfg.logPath)
45027
44960
  return;
45028
- const line = `${JSON.stringify({ ts: new Date().toISOString(), ...event })}
45029
- `;
45030
44961
  try {
45031
- appendFileSync7(cfg.logPath, line);
44962
+ logAdvisorMarkers(cfg, JSON.stringify(body));
45032
44963
  } catch {}
45033
44964
  }
45034
- function recordAdvisorEventsFromChunk(cfg, chunkText) {
45035
- extractAdvisorToolUseIds(chunkText);
44965
+ function logAdvisorMarkers(cfg, text) {
45036
44966
  if (!cfg.logPath)
45037
44967
  return;
45038
44968
  const markers = [
@@ -45046,37 +44976,275 @@ function recordAdvisorEventsFromChunk(cfg, chunkText) {
45046
44976
  for (const [needle, kind] of markers) {
45047
44977
  let i = 0;
45048
44978
  while (true) {
45049
- i = chunkText.indexOf(needle, i);
44979
+ i = text.indexOf(needle, i);
45050
44980
  if (i < 0)
45051
44981
  break;
45052
- const ctx = chunkText.slice(Math.max(0, i - 40), i + 160);
44982
+ const ctx = text.slice(Math.max(0, i - 40), i + 160);
45053
44983
  logAdvisorEvent(cfg, { kind, needle, ctx });
45054
44984
  i += needle.length;
45055
44985
  }
45056
44986
  }
45057
44987
  }
45058
- function extractAdvisorToolUseIds(chunkText) {
45059
- const re = /"type"\s*:\s*"tool_use"\s*,\s*"id"\s*:\s*"(toolu_[A-Za-z0-9_-]+)"\s*,\s*"name"\s*:\s*"advisor"/g;
45060
- let m;
45061
- while ((m = re.exec(chunkText)) !== null) {
45062
- rememberAdvisorToolUseId(m[1]);
44988
+ function warnAdvisor(message) {
44989
+ logStderr(`[advisor] ${message}`);
44990
+ }
44991
+ function sessionKeyFor(sessionId) {
44992
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : NO_SESSION_BUCKET;
44993
+ }
44994
+ function isCallInFlight(toolUseId) {
44995
+ return (inFlightByToolUseId.get(toolUseId) ?? 0) > 0;
44996
+ }
44997
+ function bucketHasInFlightCall(bucket) {
44998
+ for (const id of bucket.keys())
44999
+ if (isCallInFlight(id))
45000
+ return true;
45001
+ return false;
45002
+ }
45003
+ function isExpired(call, now) {
45004
+ if (isCallInFlight(call.toolUseId))
45005
+ return false;
45006
+ return now - call.lastSeenAt > ADVISOR_PENDING_LIMITS.ttlMs;
45007
+ }
45008
+ function sweepExpiredIfDue() {
45009
+ const now = clock();
45010
+ if (now - lastSweepAt < SWEEP_INTERVAL_MS)
45011
+ return;
45012
+ lastSweepAt = now;
45013
+ for (const [key, bucket] of pendingBySession) {
45014
+ for (const [id, call] of bucket) {
45015
+ if (isExpired(call, now))
45016
+ bucket.delete(id);
45017
+ }
45018
+ if (bucket.size === 0)
45019
+ pendingBySession.delete(key);
45020
+ }
45021
+ }
45022
+ function touchBucket(key) {
45023
+ const bucket = pendingBySession.get(key);
45024
+ if (!bucket)
45025
+ return;
45026
+ pendingBySession.delete(key);
45027
+ pendingBySession.set(key, bucket);
45028
+ return bucket;
45029
+ }
45030
+ function bucketForWrite(key) {
45031
+ const existing = touchBucket(key);
45032
+ if (existing)
45033
+ return existing;
45034
+ const bucket = new Map;
45035
+ pendingBySession.set(key, bucket);
45036
+ while (pendingBySession.size > ADVISOR_PENDING_LIMITS.maxSessions) {
45037
+ const evictable = [...pendingBySession].find(([k, b]) => k !== key && !bucketHasInFlightCall(b));
45038
+ if (!evictable)
45039
+ break;
45040
+ pendingBySession.delete(evictable[0]);
45041
+ }
45042
+ return bucket;
45043
+ }
45044
+ function putCall(bucket, call) {
45045
+ bucket.delete(call.toolUseId);
45046
+ if (bucket.size >= ADVISOR_PENDING_LIMITS.maxCallsPerSession) {
45047
+ const oldest = [...bucket.keys()].find((id) => !isCallInFlight(id));
45048
+ if (oldest !== undefined)
45049
+ bucket.delete(oldest);
45050
+ }
45051
+ bucket.set(call.toolUseId, call);
45052
+ }
45053
+ function rememberAdvisorToolUseId(id, sessionId) {
45054
+ sweepExpiredIfDue();
45055
+ const key = sessionKeyFor(sessionId);
45056
+ const bucket = bucketForWrite(key);
45057
+ const now = clock();
45058
+ const existing = bucket.get(id);
45059
+ if (existing) {
45060
+ existing.lastSeenAt = now;
45061
+ putCall(bucket, existing);
45062
+ return;
45063
45063
  }
45064
- const re2 = /"name"\s*:\s*"advisor"[^}]*?"id"\s*:\s*"(toolu_[A-Za-z0-9_-]+)"/g;
45065
- while ((m = re2.exec(chunkText)) !== null) {
45066
- rememberAdvisorToolUseId(m[1]);
45064
+ putCall(bucket, { toolUseId: id, sessionKey: key, recordedAt: now, lastSeenAt: now });
45065
+ log(`[advisor] recorded advisor tool_use ${id} (session=${key})`);
45066
+ }
45067
+ function lookupAdvisorCall(toolUseId, sessionId) {
45068
+ sweepExpiredIfDue();
45069
+ const now = clock();
45070
+ const own = sessionKeyFor(sessionId);
45071
+ const keys = own === NO_SESSION_BUCKET ? [NO_SESSION_BUCKET] : [own, NO_SESSION_BUCKET];
45072
+ for (const key of keys) {
45073
+ const bucket = pendingBySession.get(key);
45074
+ const call = bucket?.get(toolUseId);
45075
+ if (!bucket || !call)
45076
+ continue;
45077
+ if (isExpired(call, now)) {
45078
+ bucket.delete(toolUseId);
45079
+ continue;
45080
+ }
45081
+ call.lastSeenAt = now;
45082
+ putCall(bucket, call);
45083
+ touchBucket(key);
45084
+ return call;
45067
45085
  }
45086
+ return;
45068
45087
  }
45069
- function rememberAdvisorToolUseId(id) {
45070
- if (advisorToolUseIds.has(id))
45088
+ function getAdvisorCall(toolUseId, sessionId) {
45089
+ return lookupAdvisorCall(toolUseId, sessionId);
45090
+ }
45091
+ function markAdvisorCallConsumed(toolUseId, result, sessionId) {
45092
+ const call = lookupAdvisorCall(toolUseId, sessionId);
45093
+ if (!call)
45094
+ return false;
45095
+ const own = sessionKeyFor(sessionId);
45096
+ if (call.sessionKey === NO_SESSION_BUCKET && own !== NO_SESSION_BUCKET) {
45097
+ pendingBySession.get(NO_SESSION_BUCKET)?.delete(toolUseId);
45098
+ call.sessionKey = own;
45099
+ putCall(bucketForWrite(own), call);
45100
+ }
45101
+ call.consumedAt ??= clock();
45102
+ call.result = result;
45103
+ return true;
45104
+ }
45105
+ function joinOrStartAdvisorCall(toolUseId, sessionId, start) {
45106
+ const key = `${sessionKeyFor(sessionId)}\x00${toolUseId}`;
45107
+ const existing = inFlightAdvisorCalls.get(key);
45108
+ if (existing)
45109
+ return { promise: existing, joined: true };
45110
+ while (inFlightAdvisorCalls.size >= MAX_IN_FLIGHT_ADVISOR_CALLS) {
45111
+ const oldest = inFlightAdvisorCalls.keys().next().value;
45112
+ if (oldest === undefined)
45113
+ break;
45114
+ inFlightAdvisorCalls.delete(oldest);
45115
+ }
45116
+ inFlightByToolUseId.set(toolUseId, (inFlightByToolUseId.get(toolUseId) ?? 0) + 1);
45117
+ const release = () => {
45118
+ const left = (inFlightByToolUseId.get(toolUseId) ?? 1) - 1;
45119
+ if (left > 0)
45120
+ inFlightByToolUseId.set(toolUseId, left);
45121
+ else
45122
+ inFlightByToolUseId.delete(toolUseId);
45123
+ };
45124
+ let started;
45125
+ try {
45126
+ started = start();
45127
+ } catch (err) {
45128
+ release();
45129
+ throw err;
45130
+ }
45131
+ const promise = started.finally(() => {
45132
+ release();
45133
+ if (inFlightAdvisorCalls.get(key) === promise)
45134
+ inFlightAdvisorCalls.delete(key);
45135
+ });
45136
+ inFlightAdvisorCalls.set(key, promise);
45137
+ return { promise, joined: false };
45138
+ }
45139
+
45140
+ class SseFrameBuffer {
45141
+ buf = "";
45142
+ take(chunkText) {
45143
+ this.buf += chunkText;
45144
+ const events = [];
45145
+ while (true) {
45146
+ const lf = this.buf.indexOf(`
45147
+
45148
+ `);
45149
+ const crlf = this.buf.indexOf(`\r
45150
+ \r
45151
+ `);
45152
+ let idx = -1;
45153
+ let width = 2;
45154
+ if (crlf >= 0 && (lf < 0 || crlf < lf)) {
45155
+ idx = crlf;
45156
+ width = 4;
45157
+ } else if (lf >= 0) {
45158
+ idx = lf;
45159
+ }
45160
+ if (idx < 0)
45161
+ break;
45162
+ events.push(this.buf.slice(0, idx));
45163
+ this.buf = this.buf.slice(idx + width);
45164
+ }
45165
+ if (this.buf.length > MAX_SSE_BUFFER_CHARS) {
45166
+ this.buf = this.buf.slice(-MAX_SSE_BUFFER_CHARS);
45167
+ }
45168
+ return events;
45169
+ }
45170
+ reset() {
45171
+ this.buf = "";
45172
+ }
45173
+ }
45174
+ function createAdvisorStreamScanner(cfg, sessionId) {
45175
+ const frames = new SseFrameBuffer;
45176
+ return {
45177
+ push(chunkText) {
45178
+ extractAdvisorToolUseIds(chunkText, frames, sessionId);
45179
+ logAdvisorMarkers(cfg, chunkText);
45180
+ }
45181
+ };
45182
+ }
45183
+ function extractAdvisorToolUseIds(chunkText, frames, sessionId) {
45184
+ if (parseAdvisorIdsFromJsonText(chunkText, sessionId))
45185
+ return;
45186
+ let sawCompleteFrame = false;
45187
+ for (const event of frames.take(chunkText)) {
45188
+ sawCompleteFrame = true;
45189
+ if (!scanSseEventForAdvisorIds(event, sessionId))
45190
+ matchAdvisorIdsByRegex(event, sessionId);
45191
+ }
45192
+ if (!sawCompleteFrame)
45193
+ matchAdvisorIdsByRegex(chunkText, sessionId);
45194
+ }
45195
+ function scanSseEventForAdvisorIds(rawEvent, sessionId) {
45196
+ const dataLines = [];
45197
+ for (const line of rawEvent.split(/\r?\n/)) {
45198
+ if (!line.startsWith("data:"))
45199
+ continue;
45200
+ dataLines.push(line.slice(5).trimStart());
45201
+ }
45202
+ if (dataLines.length === 0)
45203
+ return false;
45204
+ const payload = dataLines.join(`
45205
+ `).trim();
45206
+ if (!payload || payload === "[DONE]")
45207
+ return true;
45208
+ return parseAdvisorIdsFromJsonText(payload, sessionId);
45209
+ }
45210
+ function parseAdvisorIdsFromJsonText(text, sessionId) {
45211
+ const trimmed = text.trimStart();
45212
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
45213
+ return false;
45214
+ let parsed;
45215
+ try {
45216
+ parsed = JSON.parse(trimmed);
45217
+ } catch {
45218
+ return false;
45219
+ }
45220
+ collectAdvisorIdsFromValue(parsed, 0, sessionId);
45221
+ return true;
45222
+ }
45223
+ function collectAdvisorIdsFromValue(value, depth, sessionId) {
45224
+ if (value === null || typeof value !== "object" || depth > MAX_WALK_DEPTH)
45225
+ return;
45226
+ if (Array.isArray(value)) {
45227
+ for (const item of value)
45228
+ collectAdvisorIdsFromValue(item, depth + 1, sessionId);
45071
45229
  return;
45072
- if (advisorToolUseIds.size >= MAX_TRACKED) {
45073
- const first = advisorToolUseIds.values().next().value;
45074
- if (first !== undefined)
45075
- advisorToolUseIds.delete(first);
45076
45230
  }
45077
- advisorToolUseIds.add(id);
45231
+ const obj = value;
45232
+ if (obj.type === "tool_use" && obj.name === ADVISOR_TOOL_NAME && typeof obj.id === "string" && obj.id.length > 0) {
45233
+ rememberAdvisorToolUseId(obj.id, sessionId);
45234
+ }
45235
+ for (const nested of Object.values(obj))
45236
+ collectAdvisorIdsFromValue(nested, depth + 1, sessionId);
45237
+ }
45238
+ function matchAdvisorIdsByRegex(text, sessionId) {
45239
+ for (const re of ADVISOR_ID_PATTERNS) {
45240
+ re.lastIndex = 0;
45241
+ let m;
45242
+ while ((m = re.exec(text)) !== null) {
45243
+ rememberAdvisorToolUseId(m[1], sessionId);
45244
+ }
45245
+ }
45078
45246
  }
45079
- function rewriteAdvisorToolResults(payload, getAdviceFor) {
45247
+ function rewriteAdvisorToolResults(payload, getAdviceFor, sessionId) {
45080
45248
  const messages = payload.messages;
45081
45249
  if (!Array.isArray(messages))
45082
45250
  return [];
@@ -45097,12 +45265,18 @@ function rewriteAdvisorToolResults(payload, getAdviceFor) {
45097
45265
  const toolUseId = block.tool_use_id;
45098
45266
  if (typeof toolUseId !== "string")
45099
45267
  continue;
45100
- if (!advisorToolUseIds.has(toolUseId))
45268
+ if (!lookupAdvisorCall(toolUseId, sessionId))
45101
45269
  continue;
45102
- const advice = getAdviceFor(toolUseId);
45103
- block.content = [{ type: "text", text: advice }];
45104
- if (block.is_error)
45270
+ const supplied = getAdviceFor(toolUseId);
45271
+ if (supplied === undefined)
45272
+ continue;
45273
+ const result = typeof supplied === "string" ? { text: supplied, isError: false } : supplied;
45274
+ block.content = [{ type: "text", text: result.text }];
45275
+ if (result.isError) {
45276
+ block.is_error = true;
45277
+ } else if (block.is_error) {
45105
45278
  block.is_error = false;
45279
+ }
45106
45280
  rewritten.push(toolUseId);
45107
45281
  }
45108
45282
  }
@@ -45111,7 +45285,34 @@ function rewriteAdvisorToolResults(payload, getAdviceFor) {
45111
45285
  function stubAdvisorAdvice(toolUseId) {
45112
45286
  return `CLAUDISH_ADVISOR_STUB_${toolUseId}: Evaluation mode \u2014 this advice was supplied by a claudish proxy stub. For the rate-limiter design, consider a hybrid: local token bucket per node for burst tolerance plus a central quota coordinator for cross-region fairness. Use the CAP tradeoff as your framing; expose availability vs accuracy knobs per tenant. The single most important decision is your failure mode: fail-open vs fail-closed.`;
45113
45287
  }
45114
- function findPendingAdvisorToolResults(payload) {
45288
+ function prepareLegacyStubResult(cfg, toolUseId, sessionId) {
45289
+ const result = { text: stubAdvisorAdvice(toolUseId), isError: false };
45290
+ markAdvisorCallConsumed(toolUseId, result, sessionId);
45291
+ logAdvisorEvent(cfg, {
45292
+ kind: "advisor_rewrite",
45293
+ event: "advisor_rewrite",
45294
+ toolUseId,
45295
+ sessionId: sessionId ?? null,
45296
+ panel: [],
45297
+ originsByModel: {},
45298
+ failedModels: [],
45299
+ collector: null,
45300
+ collectorOrigin: null,
45301
+ resultOrigin: "stub",
45302
+ stubPath: ADVISOR_STUB_PATHS.LEGACY_STUB,
45303
+ isError: false
45304
+ });
45305
+ return result;
45306
+ }
45307
+ function missingAdvisorResult(toolUseId) {
45308
+ log(`[advisor] no prepared result for recorded call ${toolUseId} (stub path S2)`);
45309
+ warnAdvisor(`advisor call ${toolUseId}: claudish had no prepared result (internal error)`);
45310
+ return {
45311
+ text: `${ADVISOR_ERROR_PREFIX} claudish recorded advisor call ${toolUseId} but had no result prepared for it (internal error). ${ADVISOR_ERROR_SUFFIX}`,
45312
+ isError: true
45313
+ };
45314
+ }
45315
+ function findPendingAdvisorToolResults(payload, sessionId) {
45115
45316
  const messages = payload.messages;
45116
45317
  if (!Array.isArray(messages))
45117
45318
  return [];
@@ -45130,13 +45331,101 @@ function findPendingAdvisorToolResults(payload) {
45130
45331
  if (block.type !== "tool_result")
45131
45332
  continue;
45132
45333
  const toolUseId = block.tool_use_id;
45133
- if (typeof toolUseId === "string" && advisorToolUseIds.has(toolUseId)) {
45334
+ if (typeof toolUseId === "string" && lookupAdvisorCall(toolUseId, sessionId)) {
45134
45335
  found.push(toolUseId);
45135
45336
  }
45136
45337
  }
45137
45338
  }
45138
45339
  return found;
45139
45340
  }
45341
+ function toolResultText(content) {
45342
+ if (typeof content === "string")
45343
+ return content;
45344
+ if (!Array.isArray(content))
45345
+ return "";
45346
+ return content.map((b) => b && typeof b.text === "string" ? b.text : "").filter(Boolean).join(`
45347
+ `);
45348
+ }
45349
+ function advisorToolUseIdsInPayload(payload) {
45350
+ const ids = new Set;
45351
+ const messages = payload.messages;
45352
+ if (!Array.isArray(messages))
45353
+ return ids;
45354
+ for (const msg of messages) {
45355
+ if (!msg || typeof msg !== "object")
45356
+ continue;
45357
+ const content = msg.content;
45358
+ if (!Array.isArray(content))
45359
+ continue;
45360
+ for (const block of content) {
45361
+ if (!block || typeof block !== "object")
45362
+ continue;
45363
+ const b = block;
45364
+ if (b.type !== "tool_use" || b.name !== ADVISOR_TOOL_NAME)
45365
+ continue;
45366
+ if (typeof b.id === "string" && b.id.length > 0)
45367
+ ids.add(b.id);
45368
+ }
45369
+ }
45370
+ return ids;
45371
+ }
45372
+ function reportUnrecordedAdvisorCalls(cfg, payload, sessionId) {
45373
+ const messages = payload.messages;
45374
+ if (!Array.isArray(messages))
45375
+ return [];
45376
+ const advisorIds = advisorToolUseIdsInPayload(payload);
45377
+ if (advisorIds.size === 0)
45378
+ return [];
45379
+ const reported = [];
45380
+ for (const msg of messages) {
45381
+ if (!msg || typeof msg !== "object" || msg.role !== "user")
45382
+ continue;
45383
+ const content = msg.content;
45384
+ if (!Array.isArray(content))
45385
+ continue;
45386
+ for (const block of content) {
45387
+ if (!block || typeof block !== "object" || block.type !== "tool_result")
45388
+ continue;
45389
+ const toolUseId = block.tool_use_id;
45390
+ if (typeof toolUseId !== "string")
45391
+ continue;
45392
+ if (!advisorIds.has(toolUseId))
45393
+ continue;
45394
+ if (!NO_SUCH_ADVISOR_TOOL.test(toolResultText(block.content)))
45395
+ continue;
45396
+ if (lookupAdvisorCall(toolUseId, sessionId))
45397
+ continue;
45398
+ const memo = `${sessionKeyFor(sessionId)}\x00${toolUseId}`;
45399
+ if (reportedUnrecorded.has(memo))
45400
+ continue;
45401
+ if (reportedUnrecorded.size >= MAX_REPORTED_UNRECORDED) {
45402
+ const oldest = reportedUnrecorded.values().next().value;
45403
+ if (oldest !== undefined)
45404
+ reportedUnrecorded.delete(oldest);
45405
+ }
45406
+ reportedUnrecorded.add(memo);
45407
+ reported.push(toolUseId);
45408
+ const panel = cfg.models ?? [];
45409
+ logAdvisorEvent(cfg, {
45410
+ kind: "advisor_rewrite",
45411
+ event: "advisor_rewrite",
45412
+ toolUseId,
45413
+ sessionId: sessionId ?? null,
45414
+ panel,
45415
+ originsByModel: Object.fromEntries(panel.map((m) => [m, "absent"])),
45416
+ failedModels: [],
45417
+ collector: cfg.collector ?? null,
45418
+ collectorOrigin: cfg.collector ? "absent" : null,
45419
+ resultOrigin: "absent",
45420
+ stubPath: ADVISOR_STUB_PATHS.NOT_REWRITTEN,
45421
+ isError: true
45422
+ });
45423
+ log(`[advisor] call ${toolUseId} was never rewritten (stub path S10)`);
45424
+ warnAdvisor(`advisor call ${toolUseId} was not answered: claudish never saw the advisor tool call, so the model got "No such tool available: advisor"`);
45425
+ }
45426
+ }
45427
+ return reported;
45428
+ }
45140
45429
  function convertToOpenAIMessages(anthropicMessages) {
45141
45430
  return anthropicMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
45142
45431
  role: m.role,
@@ -45164,168 +45453,598 @@ function extractBlocksAsText(content) {
45164
45453
  }).filter(Boolean).join(`
45165
45454
  `);
45166
45455
  }
45167
- function buildAdvisorRequest(parsed, messages, apiKeys, systemPrompt = ADVISOR_SYSTEM_PROMPT) {
45168
- const openaiMessages = convertToOpenAIMessages(messages);
45169
- const provider = parsed.provider;
45170
- if (provider === "google" || provider === "gemini") {
45171
- return {
45172
- url: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
45173
- headers: {
45174
- "Content-Type": "application/json",
45175
- Authorization: `Bearer ${apiKeys.google ?? ""}`
45176
- },
45177
- body: {
45178
- model: parsed.model,
45179
- max_tokens: 2048,
45180
- messages: [{ role: "system", content: systemPrompt }, ...openaiMessages]
45181
- }
45182
- };
45456
+ function describeAdvisorQuestion(input) {
45457
+ const clip = (s) => s.length > MAX_ADVISOR_QUESTION_CHARS ? `${s.slice(0, MAX_ADVISOR_QUESTION_CHARS)}\u2026` : s || null;
45458
+ if (typeof input === "string")
45459
+ return clip(input.trim());
45460
+ if (!input || typeof input !== "object" || Array.isArray(input))
45461
+ return null;
45462
+ const obj = input;
45463
+ for (const field of ADVISOR_QUESTION_FIELDS) {
45464
+ const value = obj[field];
45465
+ if (typeof value === "string" && value.trim().length > 0)
45466
+ return clip(value.trim());
45183
45467
  }
45184
- if (provider === "openai" || provider === "oai") {
45185
- return {
45186
- url: "https://api.openai.com/v1/chat/completions",
45187
- headers: {
45188
- "Content-Type": "application/json",
45189
- Authorization: `Bearer ${apiKeys.openai ?? ""}`
45190
- },
45191
- body: {
45192
- model: parsed.model,
45193
- max_tokens: 2048,
45194
- messages: [{ role: "system", content: systemPrompt }, ...openaiMessages]
45468
+ if (Object.keys(obj).length === 0)
45469
+ return null;
45470
+ try {
45471
+ return clip(JSON.stringify(obj));
45472
+ } catch {
45473
+ return null;
45474
+ }
45475
+ }
45476
+ function advisorQuestionMessage(question) {
45477
+ const body = question === null ? "The assistant named no explicit question, so its question is the conversation itself: " + "advise it on the decision it now faces, the risks in the approach it has taken, and what " + "it should do next." : `The assistant's question is:
45478
+
45479
+ ${question}`;
45480
+ return "[claudish] The coding assistant paused the work above and consulted its advisor. " + `You are the advisor.
45481
+
45482
+ ` + `${body}
45483
+
45484
+ ` + "Answer that question, using the conversation above as context. The advisor tool is provided " + "by the claudish proxy and has no implementation inside the assistant's harness, so any tool " + "error about it is plumbing: do not treat it as the subject, and do not comment on how the " + "call was made.";
45485
+ }
45486
+ function prepareAdvisorPanelMessages(messages, toolUseId) {
45487
+ if (!Array.isArray(messages))
45488
+ return [];
45489
+ let question = null;
45490
+ const prepared = messages.map((msg) => {
45491
+ if (!msg || typeof msg !== "object" || !Array.isArray(msg.content))
45492
+ return msg;
45493
+ let changed = false;
45494
+ const content = msg.content.map((block) => {
45495
+ if (!block || typeof block !== "object")
45496
+ return block;
45497
+ if (block.type === "tool_use" && block.name === ADVISOR_TOOL_NAME && block.id === toolUseId) {
45498
+ question ??= describeAdvisorQuestion(block.input);
45499
+ return block;
45195
45500
  }
45196
- };
45501
+ if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
45502
+ changed = true;
45503
+ return {
45504
+ ...block,
45505
+ content: [{ type: "text", text: ADVISOR_PLUMBING_MARKER }],
45506
+ is_error: false
45507
+ };
45508
+ }
45509
+ return block;
45510
+ });
45511
+ return changed ? { ...msg, content } : msg;
45512
+ });
45513
+ prepared.push({
45514
+ role: "user",
45515
+ content: [{ type: "text", text: advisorQuestionMessage(question) }]
45516
+ });
45517
+ return prepared;
45518
+ }
45519
+ function routeOf(kind, wireModel) {
45520
+ const url = ADVISOR_ENDPOINTS[kind];
45521
+ return { kind, host: new URL(url).host, url, credential: kind, wireModel };
45522
+ }
45523
+ function advisorRouteFor(modelSpec, role) {
45524
+ const parsed = parseModelSpec(modelSpec);
45525
+ if (role === "collector" && isAnthropicModel(parsed)) {
45526
+ const model = parsed.model;
45527
+ const tier = anthropicTierAlias(model);
45528
+ if (!tier)
45529
+ return routeOf("anthropic", model);
45530
+ const resolved = findEntryByAlias(model)?.modelId ?? latestAnthropicTierModelId(tier);
45531
+ if (!resolved)
45532
+ return { ...routeOf("anthropic", model), unresolvedAlias: model };
45533
+ return routeOf("anthropic", resolved);
45197
45534
  }
45198
- const rawModelId = parsed.isExplicitProvider && provider !== "openrouter" ? `${provider}/${parsed.model}` : parsed.model;
45199
- const modelId = resolveModelNameSync("openrouter", rawModelId) ?? rawModelId;
45200
- return {
45201
- url: "https://openrouter.ai/api/v1/chat/completions",
45202
- headers: {
45203
- "Content-Type": "application/json",
45204
- Authorization: `Bearer ${apiKeys.openrouter ?? ""}`,
45205
- "HTTP-Referer": "https://claudish.com",
45206
- "X-Title": "Claudish Advisor"
45207
- },
45208
- body: {
45209
- model: modelId,
45210
- max_tokens: 2048,
45211
- messages: [{ role: "system", content: systemPrompt }, ...openaiMessages]
45535
+ const provider = parsed.provider;
45536
+ if (provider === "google" || provider === "gemini")
45537
+ return routeOf("google", parsed.model);
45538
+ if (provider === "openai" || provider === "oai")
45539
+ return routeOf("openai", parsed.model);
45540
+ if (!parsed.isExplicitProvider || provider === "openrouter") {
45541
+ return routeOf("openrouter", openRouterWireModelFor(null, parsed.model));
45542
+ }
45543
+ return routeOf("openrouter", openRouterWireModelFor(provider, parsed.model));
45544
+ }
45545
+ function openRouterIdOf(entry) {
45546
+ const fromAggregator = entry.aggregators?.find((a) => a.provider === "openrouter")?.externalId;
45547
+ if (fromAggregator)
45548
+ return fromAggregator;
45549
+ return entry.sources["openrouter-api"]?.externalId ?? null;
45550
+ }
45551
+ function lookupOpenRouterId(name) {
45552
+ const entries = getCatalogEntries();
45553
+ if (!entries)
45554
+ return { kind: "unknown" };
45555
+ const lower = name.toLowerCase();
45556
+ if (name.includes("/")) {
45557
+ const match = entries.find((e) => openRouterIdOf(e)?.toLowerCase() === lower);
45558
+ return { kind: "id", id: match ? openRouterIdOf(match) : name };
45559
+ }
45560
+ const byModelId = entries.find((e) => e.modelId.toLowerCase() === lower);
45561
+ if (byModelId) {
45562
+ const id = openRouterIdOf(byModelId);
45563
+ return id ? { kind: "id", id } : { kind: "not-served" };
45564
+ }
45565
+ const byAlias = entries.find((e) => e.aliases.some((a) => a.toLowerCase() === lower));
45566
+ if (byAlias) {
45567
+ const id = openRouterIdOf(byAlias);
45568
+ return id ? { kind: "id", id } : { kind: "not-served" };
45569
+ }
45570
+ const suffix = `/${lower}`;
45571
+ for (const entry of entries) {
45572
+ const id = openRouterIdOf(entry);
45573
+ if (id && (id.toLowerCase() === lower || id.toLowerCase().endsWith(suffix))) {
45574
+ return { kind: "id", id };
45575
+ }
45576
+ }
45577
+ return { kind: "unknown" };
45578
+ }
45579
+ function isOpenRouterVendorNamespace(vendor) {
45580
+ const entries = getCatalogEntries();
45581
+ if (!entries)
45582
+ return false;
45583
+ const prefix = `${vendor.toLowerCase()}/`;
45584
+ return entries.some((e) => openRouterIdOf(e)?.toLowerCase().startsWith(prefix));
45585
+ }
45586
+ function nativeVendorsForProvider(providerUid) {
45587
+ const vendors = new Set;
45588
+ try {
45589
+ for (const plan of readAllModelsCache()?.plans ?? []) {
45590
+ if (plan.routing?.providerUid !== providerUid)
45591
+ continue;
45592
+ for (const native of plan.routing.nativeModelProviders ?? [])
45593
+ vendors.add(native);
45212
45594
  }
45595
+ } catch {}
45596
+ return [...vendors];
45597
+ }
45598
+ function openRouterWireModelFor(provider, model) {
45599
+ const lookup = lookupOpenRouterId(model);
45600
+ if (provider === null || provider === "openrouter") {
45601
+ if (lookup.kind === "id")
45602
+ return lookup.id;
45603
+ if (lookup.kind === "unknown")
45604
+ return model;
45605
+ throw new Error(`${model} cannot be used as an advisor model: the catalog lists no OpenRouter id for it ` + "(OpenRouter is where every unprefixed advisor model is called), so claudish has no id " + "OpenRouter would accept. Name a model OpenRouter serves, or give the id in full " + '("openrouter@vendor/model").');
45606
+ }
45607
+ const vendorPrefixed = `${provider}/${model}`;
45608
+ if (isOpenRouterVendorNamespace(provider))
45609
+ return vendorPrefixed;
45610
+ if (lookup.kind === "id")
45611
+ return lookup.id;
45612
+ for (const vendor of nativeVendorsForProvider(provider)) {
45613
+ if (isOpenRouterVendorNamespace(vendor))
45614
+ return `${vendor}/${model}`;
45615
+ }
45616
+ if (getCatalogEntries() === null)
45617
+ return vendorPrefixed;
45618
+ throw new Error(`${provider}@${model} cannot be resolved to a model OpenRouter serves: "${provider}" is not an ` + "OpenRouter vendor namespace, and the catalog lists no OpenRouter id for " + `"${model}". Advisor panel models are always called metered, so a subscription prefix ` + `buys nothing here \u2014 use the bare model name ("${model}"), or name the OpenRouter id ` + 'in full ("openrouter@vendor/model").');
45619
+ }
45620
+ function advisorCredentialsFor(models, collector) {
45621
+ const needed = new Set;
45622
+ const add = (spec, role) => {
45623
+ try {
45624
+ needed.add(advisorRouteFor(spec, role).credential);
45625
+ } catch {}
45213
45626
  };
45627
+ for (const m of models)
45628
+ add(m, "panel");
45629
+ if (collector && models.length > 1)
45630
+ add(collector, "collector");
45631
+ return needed;
45214
45632
  }
45215
- async function callAdvisorModel(modelSpec, messages, apiKeys) {
45216
- const parsed = parseModelSpec(modelSpec);
45217
- const { url, headers, body } = buildAdvisorRequest(parsed, messages, apiKeys);
45218
- const controller = new AbortController;
45219
- const timeout = setTimeout(() => controller.abort(), 60000);
45633
+ function isPlaceholderAnthropicKey(key) {
45634
+ return /placeholder/i.test(key);
45635
+ }
45636
+ async function resolveAdvisorCredential(credential) {
45637
+ const fromAuthority = async (provider, header) => {
45638
+ try {
45639
+ const auth = await credentials.getRequestAuth(provider, { model: "" });
45640
+ const bearer = auth.headers.Authorization?.replace(/^Bearer\s+/i, "").trim();
45641
+ const apiKey = auth.headers["x-api-key"]?.trim();
45642
+ return (header === "x-api-key" ? apiKey : bearer || apiKey) || undefined;
45643
+ } catch {
45644
+ return;
45645
+ }
45646
+ };
45647
+ if (credential === "anthropic") {
45648
+ const key = await fromAuthority(ADVISOR_AUTHORITY_PROVIDER.anthropic, "x-api-key");
45649
+ if (!key || key === process.env.ANTHROPIC_AUTH_TOKEN || isPlaceholderAnthropicKey(key)) {
45650
+ return;
45651
+ }
45652
+ return key;
45653
+ }
45654
+ if (credential === "google") {
45655
+ return await fromAuthority(ADVISOR_AUTHORITY_PROVIDER.google, "any") || process.env.GOOGLE_API_KEY?.trim() || undefined;
45656
+ }
45657
+ return fromAuthority(ADVISOR_AUTHORITY_PROVIDER[credential], "any");
45658
+ }
45659
+ function advisorTokenParamFor(route) {
45660
+ if (route.kind !== "openai")
45661
+ return "max_tokens";
45662
+ let fromCatalog;
45220
45663
  try {
45221
- const resp = await fetch(url, {
45222
- method: "POST",
45223
- headers,
45224
- body: JSON.stringify(body),
45225
- signal: controller.signal
45226
- });
45227
- if (!resp.ok) {
45228
- const errText = await resp.text().catch(() => "");
45229
- throw new Error(`${resp.status}: ${errText.slice(0, 200)}`);
45664
+ fromCatalog = lookupModelTokenParam(route.wireModel);
45665
+ } catch {}
45666
+ if (fromCatalog === "max_tokens" || fromCatalog === "max_completion_tokens")
45667
+ return fromCatalog;
45668
+ return "max_completion_tokens";
45669
+ }
45670
+ function buildAdvisorRequest(route, messages, apiKeys, systemPrompt = ADVISOR_SYSTEM_PROMPT) {
45671
+ if (route.kind === "anthropic") {
45672
+ throw new Error(`${route.wireModel}: the Anthropic route is collector-only`);
45673
+ }
45674
+ const headers = {
45675
+ "Content-Type": "application/json",
45676
+ Authorization: `Bearer ${apiKeys[route.credential] ?? ""}`
45677
+ };
45678
+ if (route.kind === "openrouter") {
45679
+ headers["HTTP-Referer"] = "https://claudish.com";
45680
+ headers["X-Title"] = "Claudish Advisor";
45681
+ }
45682
+ const body = {
45683
+ model: route.wireModel,
45684
+ messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAIMessages(messages)]
45685
+ };
45686
+ body[advisorTokenParamFor(route)] = ADVISOR_MAX_OUTPUT_TOKENS;
45687
+ return { headers, body };
45688
+ }
45689
+ function stubOutcome(base, stubPath, reason, latencyMs, secrets, observed) {
45690
+ return {
45691
+ ...base,
45692
+ upstreamStatus: observed?.status ?? null,
45693
+ upstreamStatusSource: observed?.source ?? null,
45694
+ responseBytes: observed?.bytes ?? 0,
45695
+ latencyMs,
45696
+ origin: "stub",
45697
+ stubPath,
45698
+ reason: sanitizeAdvisorReason(reason, secrets)
45699
+ };
45700
+ }
45701
+ function summarizeErrorBody(bodyText) {
45702
+ let message = "";
45703
+ try {
45704
+ message = extractProviderMessage(JSON.parse(bodyText));
45705
+ } catch {
45706
+ message = bodyText;
45707
+ }
45708
+ const oneLine = String(message ?? "").replace(/\s+/g, " ").trim();
45709
+ return oneLine ? oneLine.slice(0, 200) : "(empty body)";
45710
+ }
45711
+ function describeFetchError(err, timeoutMs) {
45712
+ const e = err;
45713
+ if (e?.name === "AbortError" && timeoutMs) {
45714
+ return `timed out after ${Math.round(timeoutMs / 1000)}s`;
45715
+ }
45716
+ return `request failed: ${e?.message ?? String(err)}`;
45717
+ }
45718
+ async function executeAdvisorFetch(plan, fetchImpl) {
45719
+ const base = { role: plan.role, requestedModel: plan.requestedModel, route: plan.route };
45720
+ const secrets = credentialValuesInHeaders(plan.headers);
45721
+ const started = performance.now();
45722
+ const elapsed = () => Math.round(performance.now() - started);
45723
+ const controller = plan.timeoutMs ? new AbortController : undefined;
45724
+ const timer = controller ? setTimeout(() => controller.abort(), plan.timeoutMs) : undefined;
45725
+ const doFetch = fetchImpl ?? fetch;
45726
+ try {
45727
+ let resp;
45728
+ try {
45729
+ resp = await doFetch(plan.route.url, {
45730
+ method: "POST",
45731
+ headers: plan.headers,
45732
+ body: JSON.stringify(plan.body),
45733
+ signal: controller?.signal
45734
+ });
45735
+ } catch (err) {
45736
+ return stubOutcome(base, plan.errorStubPath, describeFetchError(err, plan.timeoutMs), elapsed(), secrets);
45230
45737
  }
45231
- const data = await resp.json();
45232
- return data.choices?.[0]?.message?.content ?? "(no response)";
45738
+ let bodyText;
45739
+ try {
45740
+ bodyText = await resp.text();
45741
+ } catch (err) {
45742
+ return stubOutcome(base, plan.errorStubPath, `HTTP ${resp.status} but the body could not be read: ${describeFetchError(err, plan.timeoutMs)}`, elapsed(), secrets, { status: resp.status, source: "http_status", bytes: 0 });
45743
+ }
45744
+ const bodyStatus = extractUpstreamStatus(bodyText);
45745
+ const observed = {
45746
+ status: bodyStatus ?? resp.status,
45747
+ source: bodyStatus !== undefined ? "error.upstream_status" : "http_status",
45748
+ bytes: new TextEncoder().encode(bodyText).byteLength
45749
+ };
45750
+ if (!resp.ok || bodyStatus !== undefined && bodyStatus >= 400) {
45751
+ return stubOutcome(base, plan.errorStubPath, `HTTP ${observed.status}: ${summarizeErrorBody(bodyText)}`, elapsed(), secrets, observed);
45752
+ }
45753
+ let data;
45754
+ try {
45755
+ data = JSON.parse(bodyText);
45756
+ } catch {
45757
+ return stubOutcome(base, plan.emptyStubPath, `HTTP ${observed.status} but the response body is not JSON`, elapsed(), secrets, observed);
45758
+ }
45759
+ const text = plan.extractText(data);
45760
+ if (typeof text !== "string" || text.trim().length === 0) {
45761
+ return stubOutcome(base, plan.emptyStubPath, `HTTP ${observed.status} but the response carried no advice text`, elapsed(), secrets, observed);
45762
+ }
45763
+ return {
45764
+ ...base,
45765
+ upstreamStatus: observed.status,
45766
+ upstreamStatusSource: observed.source,
45767
+ responseBytes: observed.bytes,
45768
+ latencyMs: elapsed(),
45769
+ origin: "upstream",
45770
+ stubPath: null,
45771
+ text
45772
+ };
45233
45773
  } finally {
45234
- clearTimeout(timeout);
45774
+ if (timer)
45775
+ clearTimeout(timer);
45776
+ }
45777
+ }
45778
+ function extractChatCompletionText(data) {
45779
+ const content = data?.choices?.[0]?.message?.content;
45780
+ if (typeof content === "string")
45781
+ return content;
45782
+ if (Array.isArray(content)) {
45783
+ const joined = content.map((p) => typeof p?.text === "string" ? p.text : "").join("");
45784
+ return joined || undefined;
45785
+ }
45786
+ return;
45787
+ }
45788
+ function extractAnthropicText(data) {
45789
+ const blocks = data?.content;
45790
+ if (!Array.isArray(blocks))
45791
+ return;
45792
+ const text = blocks.find((b) => b?.type === "text")?.text;
45793
+ return typeof text === "string" ? text : undefined;
45794
+ }
45795
+ function errorMessageOf(err) {
45796
+ return err?.message ?? String(err);
45797
+ }
45798
+ async function callAdvisorModel(modelSpec, messages, apiKeys, fetchImpl) {
45799
+ let plan;
45800
+ try {
45801
+ const route = advisorRouteFor(modelSpec, "panel");
45802
+ const { headers, body } = buildAdvisorRequest(route, messages, apiKeys);
45803
+ plan = {
45804
+ role: "panel",
45805
+ requestedModel: modelSpec,
45806
+ route,
45807
+ headers,
45808
+ body,
45809
+ timeoutMs: ADVISOR_PANEL_TIMEOUT_MS,
45810
+ extractText: extractChatCompletionText,
45811
+ emptyStubPath: ADVISOR_STUB_PATHS.PANEL_EMPTY,
45812
+ errorStubPath: ADVISOR_STUB_PATHS.PANEL_ERROR
45813
+ };
45814
+ } catch (err) {
45815
+ const base = { role: "panel", requestedModel: modelSpec, route: null };
45816
+ return stubOutcome(base, ADVISOR_STUB_PATHS.PANEL_ERROR, `could not build the request: ${errorMessageOf(err)}`, 0, credentialValuesOf(apiKeys));
45235
45817
  }
45818
+ return executeAdvisorFetch(plan, fetchImpl);
45819
+ }
45820
+ function anthropicTierAlias(model) {
45821
+ const m = model.toLowerCase();
45822
+ return m === "opus" || m === "sonnet" || m === "haiku" ? m : null;
45236
45823
  }
45237
45824
  function isAnthropicModel(parsed) {
45238
45825
  const m = parsed.model.toLowerCase();
45239
45826
  return parsed.provider === "anthropic" || m.startsWith("claude-") || m === "haiku" || m === "sonnet" || m === "opus";
45240
45827
  }
45241
- async function callAnthropicCollector(model, adviceText, apiKey) {
45242
- const aliasResolved = model === "haiku" || model === "sonnet" || model === "opus" ? findEntryByAlias(model)?.modelId : null;
45243
- const resolvedModel = aliasResolved ?? model;
45244
- const resp = await fetch("https://api.anthropic.com/v1/messages", {
45245
- method: "POST",
45828
+ function planAnthropicCollector(requestedModel, route, adviceText, apiKey) {
45829
+ return {
45830
+ role: "collector",
45831
+ requestedModel,
45832
+ route,
45246
45833
  headers: {
45247
45834
  "Content-Type": "application/json",
45248
45835
  "x-api-key": apiKey ?? "",
45249
45836
  "anthropic-version": "2023-06-01"
45250
45837
  },
45251
- body: JSON.stringify({
45252
- model: resolvedModel,
45838
+ body: {
45839
+ model: route.wireModel,
45253
45840
  max_tokens: 1024,
45254
45841
  system: COLLECTOR_SYSTEM_PROMPT,
45255
45842
  messages: [{ role: "user", content: adviceText }]
45256
- })
45257
- });
45258
- if (!resp.ok)
45259
- throw new Error(`anthropic collector ${resp.status}`);
45260
- const data = await resp.json();
45261
- return data.content?.find((b) => b.type === "text")?.text ?? "(empty)";
45843
+ },
45844
+ timeoutMs: ADVISOR_COLLECTOR_TIMEOUT_MS,
45845
+ extractText: extractAnthropicText,
45846
+ emptyStubPath: ADVISOR_STUB_PATHS.ANTHROPIC_COLLECTOR_EMPTY,
45847
+ errorStubPath: ADVISOR_STUB_PATHS.COLLECTOR_FAILED
45848
+ };
45262
45849
  }
45263
- async function callCollectorModel(collectorSpec, advice, apiKeys) {
45264
- const adviceText = advice.map((a, i) => `### Advisor ${i + 1} (${a.model})
45850
+ async function callCollectorModel(collectorSpec, advice, apiKeys, fetchImpl) {
45851
+ let plan;
45852
+ try {
45853
+ const adviceText = advice.map((a, i) => `### Advisor ${i + 1} (${a.model})
45265
45854
  ${a.text}`).join(`
45266
45855
 
45267
45856
  `);
45268
- const parsed = parseModelSpec(collectorSpec);
45269
- if (isAnthropicModel(parsed)) {
45270
- return callAnthropicCollector(parsed.model, adviceText, apiKeys.anthropic);
45271
- }
45272
- const collectorMessages = [{ role: "user", content: adviceText }];
45273
- const { url, headers, body } = buildAdvisorRequest(parsed, collectorMessages, apiKeys, COLLECTOR_SYSTEM_PROMPT);
45274
- body.messages = [
45275
- { role: "system", content: COLLECTOR_SYSTEM_PROMPT },
45276
- { role: "user", content: adviceText }
45277
- ];
45278
- const controller = new AbortController;
45279
- const timeout = setTimeout(() => controller.abort(), 30000);
45280
- try {
45281
- const resp = await fetch(url, {
45282
- method: "POST",
45283
- headers,
45284
- body: JSON.stringify(body),
45285
- signal: controller.signal
45286
- });
45287
- if (!resp.ok)
45288
- throw new Error(`collector ${resp.status}`);
45289
- const data = await resp.json();
45290
- return data.choices?.[0]?.message?.content ?? "(collector returned empty)";
45291
- } finally {
45292
- clearTimeout(timeout);
45293
- }
45294
- }
45295
- async function fetchMultiModelAdvice(_toolUseId, messages, models, collector, apiKeys) {
45296
- const results = await Promise.allSettled(models.map((model) => callAdvisorModel(model, messages, apiKeys)));
45297
- const sections = [];
45298
- const successfulAdvice = [];
45299
- for (let i = 0;i < models.length; i++) {
45300
- const result = results[i];
45301
- if (result.status === "fulfilled") {
45302
- sections.push(`## ${models[i]}
45303
- ${result.value}`);
45304
- successfulAdvice.push({ model: models[i], text: result.value });
45857
+ const route = advisorRouteFor(collectorSpec, "collector");
45858
+ if (route.unresolvedAlias) {
45859
+ throw new Error(`"${route.unresolvedAlias}" is a claudish alias, not a model id ${route.host} accepts, and ` + "the model catalog holds no id for it, so claudish refused to send it. Name the " + 'collector by its full model id (for example "claude-haiku-4-5"), or refresh the ' + "catalog (`claudish --models-refresh`).");
45860
+ }
45861
+ if (route.kind === "anthropic") {
45862
+ plan = planAnthropicCollector(collectorSpec, route, adviceText, apiKeys.anthropic);
45305
45863
  } else {
45306
- sections.push(`## ${models[i]}
45307
- [Error: ${result.reason?.message ?? "unknown"}]`);
45864
+ const { headers, body } = buildAdvisorRequest(route, [], apiKeys, COLLECTOR_SYSTEM_PROMPT);
45865
+ body.messages = [
45866
+ { role: "system", content: COLLECTOR_SYSTEM_PROMPT },
45867
+ { role: "user", content: adviceText }
45868
+ ];
45869
+ plan = {
45870
+ role: "collector",
45871
+ requestedModel: collectorSpec,
45872
+ route,
45873
+ headers,
45874
+ body,
45875
+ timeoutMs: ADVISOR_COLLECTOR_TIMEOUT_MS,
45876
+ extractText: extractChatCompletionText,
45877
+ emptyStubPath: ADVISOR_STUB_PATHS.COLLECTOR_EMPTY,
45878
+ errorStubPath: ADVISOR_STUB_PATHS.COLLECTOR_FAILED
45879
+ };
45308
45880
  }
45881
+ } catch (err) {
45882
+ const base = {
45883
+ role: "collector",
45884
+ requestedModel: collectorSpec,
45885
+ route: null
45886
+ };
45887
+ return stubOutcome(base, ADVISOR_STUB_PATHS.COLLECTOR_FAILED, `could not build the request: ${errorMessageOf(err)}`, 0, credentialValuesOf(apiKeys));
45309
45888
  }
45310
- if (models.length === 1 && successfulAdvice.length === 1) {
45311
- return successfulAdvice[0].text;
45889
+ return executeAdvisorFetch(plan, fetchImpl);
45890
+ }
45891
+ function panelFailureSection(o) {
45892
+ return `## ${o.requestedModel}
45893
+ [Error: ${o.requestedModel} returned no advice \u2014 ${o.reason}]`;
45894
+ }
45895
+ function allPanelFailedText(panel) {
45896
+ if (panel.length === 0) {
45897
+ return `${ADVISOR_ERROR_PREFIX} No advisor model is configured, so no advice was produced. ${ADVISOR_ERROR_SUFFIX}`;
45312
45898
  }
45313
- if (!collector || successfulAdvice.length === 0) {
45314
- return sections.join(`
45899
+ const head = panel.length === 1 ? `The advisor model ${panel[0].requestedModel} did not return advice.` : `None of the ${panel.length} advisor models returned advice.`;
45900
+ const lines = panel.map((o) => `- ${o.requestedModel}: ${o.reason}`);
45901
+ return `${ADVISOR_ERROR_PREFIX} ${head}
45902
+ ${lines.join(`
45903
+ `)}
45904
+ ${ADVISOR_ERROR_SUFFIX}`;
45905
+ }
45906
+ function collectorFailedText(collector, sections) {
45907
+ return `${ADVISOR_ERROR_PREFIX} The collector model ${collector.requestedModel} failed to synthesize ` + `the advice: ${collector.reason}. The panel's unsynthesized answers follow.
45315
45908
 
45316
- `);
45317
- }
45318
- try {
45319
- const synthesized = await callCollectorModel(collector, successfulAdvice, apiKeys);
45320
- return synthesized;
45321
- } catch (err) {
45322
- log(`[advisor] collector ${collector} failed: ${err.message}, falling back to concat`);
45323
- return sections.join(`
45909
+ ` + sections.join(`
45324
45910
 
45325
45911
  `);
45912
+ }
45913
+ function modelCallRecord(kind, outcome, call) {
45914
+ return {
45915
+ kind,
45916
+ event: kind,
45917
+ toolUseId: call.toolUseId,
45918
+ sessionId: call.sessionId,
45919
+ role: outcome.role,
45920
+ requestedModel: outcome.requestedModel,
45921
+ provider: outcome.route?.kind ?? null,
45922
+ route: outcome.route ? { kind: outcome.route.kind, host: outcome.route.host } : null,
45923
+ routedModel: outcome.route?.wireModel ?? null,
45924
+ upstreamStatus: outcome.upstreamStatus,
45925
+ upstreamStatusSource: outcome.upstreamStatusSource,
45926
+ responseBytes: outcome.responseBytes,
45927
+ latencyMs: outcome.latencyMs,
45928
+ origin: outcome.origin,
45929
+ stubPath: outcome.stubPath,
45930
+ reason: outcome.reason ?? null
45931
+ };
45932
+ }
45933
+ function logAdvisorCallOutcome(cfg, o) {
45934
+ for (const p of o.panel)
45935
+ logAdvisorEvent(cfg, modelCallRecord("advisor_call", p, o));
45936
+ if (o.collectorOutcome) {
45937
+ logAdvisorEvent(cfg, modelCallRecord("advisor_collector_call", o.collectorOutcome, o));
45938
+ }
45939
+ const failedModels = [...o.panel, ...o.collectorOutcome ? [o.collectorOutcome] : []].filter((m) => m.origin !== "upstream").map((m) => m.requestedModel);
45940
+ logAdvisorEvent(cfg, {
45941
+ kind: "advisor_rewrite",
45942
+ event: "advisor_rewrite",
45943
+ toolUseId: o.toolUseId,
45944
+ sessionId: o.sessionId,
45945
+ panel: o.panel.map((p) => p.requestedModel),
45946
+ originsByModel: Object.fromEntries(o.panel.map((p) => [p.requestedModel, p.origin])),
45947
+ failedModels,
45948
+ collector: o.collector,
45949
+ collectorOrigin: o.collectorOutcome ? o.collectorOutcome.origin : o.collector ? "absent" : null,
45950
+ resultOrigin: o.resultOrigin,
45951
+ stubPath: o.stubPath,
45952
+ isError: o.result.isError
45953
+ });
45954
+ }
45955
+ function warnOnAdvisorFailures(o, warn = warnAdvisor) {
45956
+ const failed = [...o.panel, ...o.collectorOutcome ? [o.collectorOutcome] : []].filter((m) => m.origin !== "upstream");
45957
+ if (failed.length === 0 && o.resultOrigin === "upstream")
45958
+ return;
45959
+ const detail = failed.length > 0 ? failed.map((m) => `${m.role === "collector" ? "collector " : ""}${m.requestedModel}: ${m.reason}`).join("; ") : "no advisor model is configured";
45960
+ const verdict = o.resultOrigin === "upstream" ? "advice from the other models was still delivered" : "the model received an error report instead of advice";
45961
+ warn(`advisor call ${o.toolUseId} \u2014 ${detail} (${verdict})`);
45962
+ }
45963
+ function recoverUnassociatedAdvisorResult(cfg, outcome, warn = warnAdvisor) {
45964
+ const lost = [
45965
+ ...outcome.panel.map((p) => p.requestedModel),
45966
+ ...outcome.collectorOutcome ? [outcome.collectorOutcome.requestedModel] : []
45967
+ ];
45968
+ const result = {
45969
+ text: `${ADVISOR_ERROR_PREFIX} claudish could not associate the advisor answer with call ` + `${outcome.toolUseId}: its record was gone by the time the answer arrived, so the advice ` + `from ${lost.length > 0 ? lost.join(", ") : "the panel"} could not be delivered. ` + ADVISOR_ERROR_SUFFIX,
45970
+ isError: true
45971
+ };
45972
+ const sessionId = outcome.sessionId ?? undefined;
45973
+ rememberAdvisorToolUseId(outcome.toolUseId, sessionId);
45974
+ markAdvisorCallConsumed(outcome.toolUseId, result, sessionId);
45975
+ log(`[advisor] call ${outcome.toolUseId}: the result could not be associated with the tracked call (stub path ${ADVISOR_STUB_PATHS.PREPARED_RESULT_MISSING})`);
45976
+ if (cfg) {
45977
+ logAdvisorEvent(cfg, {
45978
+ kind: "advisor_rewrite",
45979
+ event: "advisor_rewrite",
45980
+ toolUseId: outcome.toolUseId,
45981
+ sessionId: outcome.sessionId,
45982
+ corrects: "advisor_rewrite",
45983
+ panel: outcome.panel.map((p) => p.requestedModel),
45984
+ originsByModel: Object.fromEntries(outcome.panel.map((p) => [p.requestedModel, "stub"])),
45985
+ failedModels: lost,
45986
+ collector: outcome.collector,
45987
+ collectorOrigin: outcome.collectorOutcome ? "stub" : null,
45988
+ resultOrigin: "stub",
45989
+ stubPath: ADVISOR_STUB_PATHS.PREPARED_RESULT_MISSING,
45990
+ isError: true
45991
+ });
45326
45992
  }
45993
+ warn(`advisor call ${outcome.toolUseId}: the panel answered, but claudish no longer had a record of the call, so the model received an error report instead of the advice`);
45994
+ return result;
45327
45995
  }
45328
- var ADVISOR_SERVER_TOOL_TYPE = "advisor_20260301", ADVISOR_BETA_FLAG = "advisor-tool-2026-03-01", advisorToolUseIds, MAX_TRACKED = 256, ADVISOR_SYSTEM_PROMPT = `You are a strategic advisor to a coding agent. You have been given the full conversation history between a user and a Claude Code coding assistant. The assistant has paused to consult you for guidance.
45996
+ async function runAdvisorCall(params) {
45997
+ const { toolUseId, messages, models, collector, apiKeys, fetchImpl } = params;
45998
+ const panelMessages = prepareAdvisorPanelMessages(messages, toolUseId);
45999
+ const panel = await Promise.all(models.map((m) => callAdvisorModel(m, panelMessages, apiKeys, fetchImpl)));
46000
+ const successful = panel.filter((o) => o.origin === "upstream");
46001
+ const sections = panel.map((o) => o.origin === "upstream" ? `## ${o.requestedModel}
46002
+ ${o.text}` : panelFailureSection(o));
46003
+ let result;
46004
+ let collectorOutcome = null;
46005
+ let resultOrigin;
46006
+ let stubPath = null;
46007
+ if (models.length === 1 && successful.length === 1) {
46008
+ result = { text: successful[0].text, isError: false };
46009
+ resultOrigin = "upstream";
46010
+ } else if (successful.length === 0) {
46011
+ result = { text: allPanelFailedText(panel), isError: true };
46012
+ resultOrigin = "stub";
46013
+ stubPath = ADVISOR_STUB_PATHS.ALL_PANEL_FAILED;
46014
+ } else if (!collector) {
46015
+ result = { text: sections.join(`
46016
+
46017
+ `), isError: false };
46018
+ resultOrigin = "upstream";
46019
+ } else {
46020
+ collectorOutcome = await callCollectorModel(collector, successful.map((o) => ({ model: o.requestedModel, text: o.text })), apiKeys, fetchImpl);
46021
+ if (collectorOutcome.origin === "upstream" && collectorOutcome.text !== undefined) {
46022
+ result = { text: collectorOutcome.text, isError: false };
46023
+ resultOrigin = "upstream";
46024
+ } else {
46025
+ log(`[advisor] collector ${collector} failed: ${collectorOutcome.reason}, falling back to concat`);
46026
+ result = { text: collectorFailedText(collectorOutcome, sections), isError: true };
46027
+ resultOrigin = "stub";
46028
+ stubPath = collectorOutcome.stubPath;
46029
+ }
46030
+ }
46031
+ const outcome = {
46032
+ toolUseId,
46033
+ sessionId: params.sessionId ?? null,
46034
+ panel,
46035
+ collector,
46036
+ collectorOutcome,
46037
+ resultOrigin,
46038
+ stubPath,
46039
+ result
46040
+ };
46041
+ log(`[advisor] call ${toolUseId}: resultOrigin=${resultOrigin}${stubPath ? ` stubPath=${stubPath}` : ""} ` + `origins=${panel.map((p) => `${p.requestedModel}:${p.origin}`).join(",")}` + (collectorOutcome ? ` collector=${collectorOutcome.requestedModel}:${collectorOutcome.origin}` : ""));
46042
+ if (params.cfg)
46043
+ logAdvisorCallOutcome(params.cfg, outcome);
46044
+ warnOnAdvisorFailures(outcome, params.warn);
46045
+ return outcome;
46046
+ }
46047
+ var ADVISOR_SERVER_TOOL_TYPE = "advisor_20260301", ADVISOR_BETA_FLAG = "advisor-tool-2026-03-01", ADVISOR_ORIGIN_LOG_PREFIX = "[advisor-origin]", ORIGIN_RECORD_KINDS, REDACTED = "[redacted]", MIN_REDACTABLE_SECRET_LENGTH = 8, ADVISOR_STUB_PATHS, ADVISOR_ERROR_PREFIX = "[claudish advisor error]", ADVISOR_ERROR_SUFFIX = "This tool result is an error report from the claudish proxy, not advice.", NO_SESSION_BUCKET = "__no_session__", ADVISOR_PENDING_LIMITS, SWEEP_INTERVAL_MS = 60000, pendingBySession, clock, lastSweepAt, ADVISOR_TOOL_NAME = "advisor", MAX_WALK_DEPTH = 32, MAX_SSE_BUFFER_CHARS, inFlightByToolUseId, inFlightAdvisorCalls, MAX_IN_FLIGHT_ADVISOR_CALLS, streamFrameBuffer, ADVISOR_ID_PATTERNS, NO_SUCH_ADVISOR_TOOL, reportedUnrecorded, MAX_REPORTED_UNRECORDED = 1024, ADVISOR_PLUMBING_MARKER = "(handled by the claudish proxy \u2014 this advisor call is what you are being asked to answer; there is no local tool output)", ADVISOR_QUESTION_FIELDS, MAX_ADVISOR_QUESTION_CHARS = 4000, ADVISOR_SYSTEM_PROMPT = `You are a strategic advisor to a coding agent. You have been given the full conversation history between a user and a Claude Code coding assistant. The assistant has paused to consult you for guidance.
45329
46048
 
45330
46049
  Review the conversation and provide concise, actionable advice. Focus on:
45331
46050
  - Architectural decisions and trade-offs
@@ -45337,13 +46056,523 @@ Be direct. Limit your response to 300-500 words.`, COLLECTOR_SYSTEM_PROMPT = `Yo
45337
46056
  - Identifies consensus points (where advisors agree)
45338
46057
  - Highlights disagreements and explains which perspective is stronger
45339
46058
  - Produces a clear, actionable recommendation
45340
- Be concise. Do not attribute advice to specific models.`;
46059
+ Be concise. Do not attribute advice to specific models.`, ADVISOR_ENDPOINTS, ADVISOR_AUTHORITY_PROVIDER, ADVISOR_MAX_OUTPUT_TOKENS = 2048, ADVISOR_PANEL_TIMEOUT_MS = 60000, ADVISOR_COLLECTOR_TIMEOUT_MS = 30000;
45341
46060
  var init_native_handler_advisor = __esm(() => {
46061
+ init_model_catalog();
46062
+ init_authority();
45342
46063
  init_logger();
46064
+ init_all_models_cache();
45343
46065
  init_catalog_client();
45344
46066
  init_catalog_query();
45345
46067
  init_model_parser();
45346
- advisorToolUseIds = new Set;
46068
+ init_anthropic_error();
46069
+ ORIGIN_RECORD_KINDS = new Set([
46070
+ "advisor_call",
46071
+ "advisor_collector_call",
46072
+ "advisor_rewrite"
46073
+ ]);
46074
+ ADVISOR_STUB_PATHS = Object.freeze({
46075
+ LEGACY_STUB: "S1",
46076
+ PREPARED_RESULT_MISSING: "S2",
46077
+ DISABLED_STUB: "S3",
46078
+ PANEL_EMPTY: "S4",
46079
+ ANTHROPIC_COLLECTOR_EMPTY: "S5",
46080
+ COLLECTOR_EMPTY: "S6",
46081
+ PANEL_ERROR: "S7",
46082
+ ALL_PANEL_FAILED: "S8",
46083
+ COLLECTOR_FAILED: "S9",
46084
+ NOT_REWRITTEN: "S10"
46085
+ });
46086
+ ADVISOR_PENDING_LIMITS = Object.freeze({
46087
+ maxCallsPerSession: 256,
46088
+ maxSessions: 64,
46089
+ ttlMs: 24 * 60 * 60 * 1000
46090
+ });
46091
+ pendingBySession = new Map;
46092
+ clock = Date.now;
46093
+ lastSweepAt = Number.NEGATIVE_INFINITY;
46094
+ MAX_SSE_BUFFER_CHARS = 256 * 1024;
46095
+ inFlightByToolUseId = new Map;
46096
+ inFlightAdvisorCalls = new Map;
46097
+ MAX_IN_FLIGHT_ADVISOR_CALLS = ADVISOR_PENDING_LIMITS.maxSessions * ADVISOR_PENDING_LIMITS.maxCallsPerSession;
46098
+ streamFrameBuffer = new SseFrameBuffer;
46099
+ ADVISOR_ID_PATTERNS = [
46100
+ /"type"\s*:\s*"tool_use"[^}]*?"id"\s*:\s*"([^"]+)"[^}]*?"name"\s*:\s*"advisor"/g,
46101
+ /"name"\s*:\s*"advisor"[^}]*?"id"\s*:\s*"([^"]+)"/g,
46102
+ /"id"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"advisor"/g
46103
+ ];
46104
+ NO_SUCH_ADVISOR_TOOL = /No such tool available:\s*advisor\b/;
46105
+ reportedUnrecorded = new Set;
46106
+ ADVISOR_QUESTION_FIELDS = [
46107
+ "question",
46108
+ "prompt",
46109
+ "query",
46110
+ "request",
46111
+ "task",
46112
+ "topic",
46113
+ "context",
46114
+ "input",
46115
+ "text"
46116
+ ];
46117
+ ADVISOR_ENDPOINTS = Object.freeze({
46118
+ google: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
46119
+ openai: "https://api.openai.com/v1/chat/completions",
46120
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
46121
+ anthropic: "https://api.anthropic.com/v1/messages"
46122
+ });
46123
+ ADVISOR_AUTHORITY_PROVIDER = Object.freeze({
46124
+ google: "google",
46125
+ openai: "openai",
46126
+ openrouter: "openrouter",
46127
+ anthropic: "native-anthropic"
46128
+ });
46129
+ });
46130
+
46131
+ // src/handlers/advisor-decorator.ts
46132
+ async function resolveAdvisorKeys(needed, inboundApiKey) {
46133
+ const anthropicKey = async () => {
46134
+ if (inboundApiKey && !isPlaceholderAnthropicKey(inboundApiKey))
46135
+ return inboundApiKey;
46136
+ return resolveAdvisorCredential("anthropic");
46137
+ };
46138
+ const [openrouter, google, openai, anthropic] = await Promise.all([
46139
+ needed.has("openrouter") ? resolveAdvisorCredential("openrouter") : undefined,
46140
+ needed.has("google") ? resolveAdvisorCredential("google") : undefined,
46141
+ needed.has("openai") ? resolveAdvisorCredential("openai") : undefined,
46142
+ needed.has("anthropic") ? anthropicKey() : undefined
46143
+ ]);
46144
+ return { openrouter, google, openai, anthropic };
46145
+ }
46146
+ function toolsOfferAdvisor(tools) {
46147
+ if (!Array.isArray(tools))
46148
+ return false;
46149
+ return tools.some((t) => {
46150
+ if (!t || typeof t !== "object")
46151
+ return false;
46152
+ const tool = t;
46153
+ return tool.type === ADVISOR_SERVER_TOOL_TYPE2 || tool.name === ADVISOR_TOOL_NAME2;
46154
+ });
46155
+ }
46156
+ function createAdvisorPresenceMonitor(warn = logStderr) {
46157
+ let consecutiveAbsent = 0;
46158
+ let warned = false;
46159
+ return {
46160
+ observe(payload) {
46161
+ if (warned)
46162
+ return;
46163
+ const tools = payload?.tools;
46164
+ if (!Array.isArray(tools) || tools.length === 0)
46165
+ return;
46166
+ if (toolsOfferAdvisor(tools)) {
46167
+ consecutiveAbsent = 0;
46168
+ return;
46169
+ }
46170
+ consecutiveAbsent++;
46171
+ log(`[advisor-swap] request offers ${tools.length} tool(s) but no advisor (${consecutiveAbsent} in a row)`);
46172
+ if (consecutiveAbsent < ADVISOR_ABSENT_WARN_AFTER)
46173
+ return;
46174
+ warned = true;
46175
+ warn(`[advisor] Claude Code sent ${consecutiveAbsent} requests in a row that offer tools but no advisor tool, so --advisor is having no effect. Claude Code is not offering the advisor in this session (for example, the experimental advisor flag was withdrawn or disabled). This warning is shown once.`);
46176
+ }
46177
+ };
46178
+ }
46179
+ function withAdvisorSwap(inner, cfg, deps = {}) {
46180
+ if (!cfg.enabled)
46181
+ return inner;
46182
+ if (inner instanceof AdvisorSwapHandler)
46183
+ return inner;
46184
+ return new AdvisorSwapHandler(inner, cfg, deps);
46185
+ }
46186
+
46187
+ class AdvisorSwapHandler {
46188
+ inner;
46189
+ cfg;
46190
+ deps;
46191
+ constructor(inner, cfg, deps = {}) {
46192
+ this.inner = inner;
46193
+ this.cfg = cfg;
46194
+ this.deps = deps;
46195
+ }
46196
+ async handle(c, payload) {
46197
+ if (c.get("advisorHandled") === true)
46198
+ return this.inner.handle(c, payload);
46199
+ c.set("advisorHandled", true);
46200
+ const sessionId = extractSessionId(payload);
46201
+ try {
46202
+ this.deps.presence?.observe(payload);
46203
+ } catch {}
46204
+ await applyAdvisorRequestSide(c, payload, this.cfg, sessionId, this.deps.resolveKeys ?? resolveAdvisorKeys);
46205
+ const response = await this.inner.handle(c, payload);
46206
+ return tapAdvisorResponse(response, this.cfg, sessionId);
46207
+ }
46208
+ async shutdown() {
46209
+ await this.inner.shutdown();
46210
+ }
46211
+ }
46212
+ async function applyAdvisorRequestSide(c, payload, cfg, sessionId, resolveKeys) {
46213
+ const target = payload.model;
46214
+ const sessionLabel = sessionId ?? NO_SESSION_BUCKET;
46215
+ const swapped = swapAdvisorToolInBody(payload);
46216
+ if (swapped) {
46217
+ c.set(ADVISOR_SWAPPED_CONTEXT_KEY, true);
46218
+ log(`[advisor-swap] replaced advisor_20260301 with function tool 'advisor' (model=${target}, session=${sessionLabel})`);
46219
+ logAdvisorEvent(cfg, {
46220
+ kind: "swap_applied",
46221
+ model: target,
46222
+ originalTool: swapped.originalTool,
46223
+ regularTool: swapped.regularTool
46224
+ });
46225
+ }
46226
+ const cachedResult = (id) => getAdvisorCall(id, sessionId)?.result;
46227
+ let rewrittenIds = [];
46228
+ if (cfg.models && cfg.models.length > 0) {
46229
+ const models = cfg.models;
46230
+ rewriteAdvisorToolResults(payload, cachedResult, sessionId);
46231
+ const pendingIds = findPendingAdvisorToolResults(payload, sessionId);
46232
+ if (pendingIds.length > 0) {
46233
+ const freshIds = [];
46234
+ const delivered = new Map;
46235
+ let apiKeys;
46236
+ for (const id of pendingIds) {
46237
+ if (cachedResult(id))
46238
+ continue;
46239
+ const runPanel = async () => {
46240
+ apiKeys ??= await resolveKeys(advisorCredentialsFor(models, cfg.collector), c.req.header("x-api-key"));
46241
+ const outcome = await runAdvisorCall({
46242
+ toolUseId: id,
46243
+ sessionId,
46244
+ messages: payload.messages,
46245
+ models,
46246
+ collector: cfg.collector ?? null,
46247
+ apiKeys,
46248
+ cfg
46249
+ });
46250
+ if (!markAdvisorCallConsumed(id, outcome.result, sessionId)) {
46251
+ return recoverUnassociatedAdvisorResult(cfg, outcome);
46252
+ }
46253
+ return outcome.result;
46254
+ };
46255
+ let { promise, joined } = joinOrStartAdvisorCall(id, sessionId, runPanel);
46256
+ if (joined) {
46257
+ log(`[advisor-swap] advisor call ${id} is already running for this session; joined it instead of running the panel again (session=${sessionLabel})`);
46258
+ try {
46259
+ delivered.set(id, await promise);
46260
+ continue;
46261
+ } catch (err) {
46262
+ log(`[advisor-swap] the in-flight advisor call ${id} failed (${errorMessage(err)}); running our own`);
46263
+ ({ promise, joined } = joinOrStartAdvisorCall(id, sessionId, runPanel));
46264
+ }
46265
+ }
46266
+ delivered.set(id, await promise);
46267
+ if (!joined)
46268
+ freshIds.push(id);
46269
+ }
46270
+ rewrittenIds = rewriteAdvisorToolResults(payload, (id) => cachedResult(id) ?? delivered.get(id) ?? missingAdvisorResult(id), sessionId);
46271
+ if (rewrittenIds.length > 0) {
46272
+ const replayed = rewrittenIds.filter((id) => !freshIds.includes(id));
46273
+ log(`[advisor-swap] rewrote ${rewrittenIds.length} advisor tool_result(s) (session=${sessionLabel}) fresh=[${freshIds.join(", ")}] replayed=[${replayed.join(", ")}] panel=[${cfg.models.join(", ")}] collector=${cfg.collector ?? "none"}`);
46274
+ logAdvisorEvent(cfg, {
46275
+ kind: "multi_model_rewrite",
46276
+ ids: rewrittenIds,
46277
+ freshIds,
46278
+ models: cfg.models,
46279
+ collector: cfg.collector,
46280
+ model: target
46281
+ });
46282
+ }
46283
+ }
46284
+ } else {
46285
+ for (const id of findPendingAdvisorToolResults(payload, sessionId)) {
46286
+ if (!cachedResult(id))
46287
+ prepareLegacyStubResult(cfg, id, sessionId);
46288
+ }
46289
+ rewrittenIds = rewriteAdvisorToolResults(payload, (id) => cachedResult(id) ?? stubAdvisorAdvice(id), sessionId);
46290
+ if (rewrittenIds.length > 0) {
46291
+ log(`[advisor-swap] rewrote ${rewrittenIds.length} advisor tool_result(s) with stub advice (session=${sessionLabel}): ${rewrittenIds.join(", ")}`);
46292
+ logAdvisorEvent(cfg, {
46293
+ kind: "tool_result_rewritten",
46294
+ ids: rewrittenIds,
46295
+ model: target
46296
+ });
46297
+ }
46298
+ }
46299
+ reportUnrecordedAdvisorCalls(cfg, payload, sessionId);
46300
+ if (cfg.dumpBodies) {
46301
+ logAdvisorEvent(cfg, {
46302
+ kind: "request_body",
46303
+ swapApplied: !!swapped,
46304
+ rewrittenIds,
46305
+ model: target,
46306
+ body: trimForLog(payload)
46307
+ });
46308
+ }
46309
+ }
46310
+ function tapAdvisorResponse(response, cfg, sessionId) {
46311
+ const body = response.body;
46312
+ if (!body)
46313
+ return response;
46314
+ const contentType = response.headers.get("content-type") ?? "";
46315
+ const kind = contentType.includes("text/event-stream") ? "sse" : contentType.includes("json") ? "json" : null;
46316
+ if (!kind)
46317
+ return response;
46318
+ let toClient;
46319
+ let toScan;
46320
+ try {
46321
+ [toClient, toScan] = body.tee();
46322
+ } catch (err) {
46323
+ log(`[advisor-swap] response not scanned (${errorMessage(err)}); passed through untouched`);
46324
+ return response;
46325
+ }
46326
+ const clientReader = toClient.getReader();
46327
+ const scanReader = toScan.getReader();
46328
+ drainScanBranch(scanReader, kind, cfg, sessionId);
46329
+ const passthrough = new ReadableStream({
46330
+ async pull(controller) {
46331
+ try {
46332
+ const { done, value } = await clientReader.read();
46333
+ if (done)
46334
+ controller.close();
46335
+ else
46336
+ controller.enqueue(value);
46337
+ } catch (err) {
46338
+ controller.error(err);
46339
+ }
46340
+ },
46341
+ cancel(reason) {
46342
+ scanReader.cancel(reason).catch(() => {});
46343
+ return clientReader.cancel(reason);
46344
+ }
46345
+ }, { highWaterMark: 0 });
46346
+ return new Response(passthrough, {
46347
+ status: response.status,
46348
+ statusText: response.statusText,
46349
+ headers: response.headers
46350
+ });
46351
+ }
46352
+ async function drainScanBranch(reader, kind, cfg, sessionId) {
46353
+ const decoder = new TextDecoder;
46354
+ const scanner = kind === "sse" ? createAdvisorStreamScanner(cfg, sessionId) : null;
46355
+ let jsonText = "";
46356
+ let jsonTooLarge = false;
46357
+ const take = (text) => {
46358
+ if (!text)
46359
+ return;
46360
+ if (scanner) {
46361
+ scanner.push(text);
46362
+ } else if (!jsonTooLarge) {
46363
+ jsonText += text;
46364
+ if (jsonText.length > MAX_JSON_SCAN_CHARS) {
46365
+ jsonTooLarge = true;
46366
+ jsonText = "";
46367
+ }
46368
+ }
46369
+ };
46370
+ try {
46371
+ while (true) {
46372
+ const { done, value } = await reader.read();
46373
+ if (done)
46374
+ break;
46375
+ take(typeof value === "string" ? value : decoder.decode(value, { stream: true }));
46376
+ }
46377
+ take(decoder.decode());
46378
+ if (jsonTooLarge) {
46379
+ log(`[advisor-swap] non-stream response over ${MAX_JSON_SCAN_CHARS} chars was not scanned for advisor calls`);
46380
+ } else if (!scanner && jsonText.trim()) {
46381
+ recordAdvisorEventsFromResponseBody(cfg, JSON.parse(jsonText), sessionId);
46382
+ }
46383
+ } catch (err) {
46384
+ log(`[advisor-swap] response scan stopped: ${errorMessage(err)} (client stream unaffected)`);
46385
+ reader.cancel(err).catch(() => {});
46386
+ }
46387
+ }
46388
+ function errorMessage(err) {
46389
+ return err instanceof Error ? err.message : String(err);
46390
+ }
46391
+ function trimForLog(payload) {
46392
+ const TEXT_TRUNC = 400;
46393
+ const clone = structuredClone(payload);
46394
+ const trimStr = (s) => typeof s === "string" && s.length > TEXT_TRUNC ? `${s.slice(0, TEXT_TRUNC)}\u2026 [+${s.length - TEXT_TRUNC} chars]` : s;
46395
+ const walk = (v) => {
46396
+ if (typeof v === "string")
46397
+ return trimStr(v);
46398
+ if (Array.isArray(v))
46399
+ return v.map(walk);
46400
+ if (v && typeof v === "object") {
46401
+ const out = {};
46402
+ for (const [k, val] of Object.entries(v))
46403
+ out[k] = walk(val);
46404
+ return out;
46405
+ }
46406
+ return v;
46407
+ };
46408
+ return walk(clone);
46409
+ }
46410
+ var ADVISOR_SWAPPED_CONTEXT_KEY = "advisorSwapped", ADVISOR_SERVER_TOOL_TYPE2 = "advisor_20260301", ADVISOR_TOOL_NAME2 = "advisor", MAX_JSON_SCAN_CHARS, ADVISOR_ABSENT_WARN_AFTER = 3;
46411
+ var init_advisor_decorator = __esm(() => {
46412
+ init_harness();
46413
+ init_logger();
46414
+ init_native_handler_advisor();
46415
+ MAX_JSON_SCAN_CHARS = 16 * 1024 * 1024;
46416
+ });
46417
+
46418
+ // src/handlers/fallback-handler.ts
46419
+ class FallbackHandler {
46420
+ candidates;
46421
+ lastSuccessIndex = 0;
46422
+ constructor(candidates) {
46423
+ this.candidates = candidates;
46424
+ }
46425
+ async handle(c, payload) {
46426
+ const errors = [];
46427
+ const startIndex = this.lastSuccessIndex;
46428
+ for (let attempt = 0;attempt < this.candidates.length; attempt++) {
46429
+ const idx = (startIndex + attempt) % this.candidates.length;
46430
+ const { name, handler } = this.candidates[idx];
46431
+ const isLast = attempt === this.candidates.length - 1;
46432
+ try {
46433
+ if (errors.length > 0 && handler instanceof ComposedHandler) {
46434
+ try {
46435
+ handler.setFallbackMeta(this.candidates.map((c) => c.name), errors.length);
46436
+ } catch {}
46437
+ }
46438
+ const response = await handler.handle(c, payload);
46439
+ if (response.ok) {
46440
+ this.lastSuccessIndex = idx;
46441
+ if (errors.length > 0) {
46442
+ logStderr(`[Fallback] ${name} succeeded after ${errors.length} failed attempt(s)`);
46443
+ if (handler instanceof ComposedHandler) {
46444
+ handler.getTokenTracker()?.setProviderDisplayName(name);
46445
+ }
46446
+ }
46447
+ return response;
46448
+ }
46449
+ const errorBody = await response.clone().text();
46450
+ if (!isRetryableError(response.status, errorBody, name)) {
46451
+ if (errors.length > 0) {
46452
+ errors.push({ provider: name, status: response.status, message: errorBody });
46453
+ return this.formatCombinedError(c, errors, payload.model);
46454
+ }
46455
+ return response;
46456
+ }
46457
+ errors.push({ provider: name, status: response.status, message: errorBody });
46458
+ if (!isLast) {
46459
+ if (hasQuotaExhaustionWording(errorBody)) {
46460
+ logStderr(`[Fallback] ${name} subscription allowance is spent \u2014 falling through to the next provider, which is billed PER TOKEN. Use a provider prefix (e.g. \`zgo@model\`) to fail instead of switching.`);
46461
+ } else {
46462
+ logStderr(`[Fallback] ${name} failed (HTTP ${response.status}), trying next provider...`);
46463
+ }
46464
+ }
46465
+ } catch (err) {
46466
+ errors.push({ provider: name, status: 0, message: err.message });
46467
+ if (!isLast) {
46468
+ logStderr(`[Fallback] ${name} error: ${err.message}, trying next provider...`);
46469
+ }
46470
+ }
46471
+ }
46472
+ return this.formatCombinedError(c, errors, payload.model);
46473
+ }
46474
+ formatCombinedError(c, errors, modelName) {
46475
+ const summary = errors.map((e) => ` ${e.provider}: HTTP ${e.status || "ERR"} \u2014 ${truncate(parseErrorMessage(e.message), 150)}`).join(`
46476
+ `);
46477
+ logStderr(`[Fallback] All ${errors.length} provider(s) failed for ${modelName || "model"}:
46478
+ ${summary}`);
46479
+ return c.json({
46480
+ error: {
46481
+ type: "all_providers_failed",
46482
+ message: `All ${errors.length} providers failed for model '${modelName || "unknown"}'`,
46483
+ attempts: errors.map((e) => ({
46484
+ provider: e.provider,
46485
+ status: e.status,
46486
+ error: truncate(parseErrorMessage(e.message), 200)
46487
+ }))
46488
+ }
46489
+ }, exhaustedChainStatus(errors));
46490
+ }
46491
+ async shutdown() {
46492
+ for (const { handler } of this.candidates) {
46493
+ if (typeof handler.shutdown === "function") {
46494
+ await handler.shutdown();
46495
+ }
46496
+ }
46497
+ }
46498
+ }
46499
+ function isRetryableError(status, errorBody, provider) {
46500
+ if (hasQuotaExhaustionWording(errorBody))
46501
+ return true;
46502
+ const upstream = status === 400 ? extractUpstreamStatus(errorBody) : undefined;
46503
+ if (upstream === 401 || upstream === 403 || upstream === 402 || upstream === 429) {
46504
+ return true;
46505
+ }
46506
+ if (status === 401 || status === 403)
46507
+ return true;
46508
+ if (status === 402)
46509
+ return true;
46510
+ if (status === 404)
46511
+ return true;
46512
+ if (status === 429)
46513
+ return true;
46514
+ const lower = errorBody.toLowerCase();
46515
+ if (status === 422) {
46516
+ if (lower.includes("not available") || lower.includes("model not found") || lower.includes("not supported")) {
46517
+ return true;
46518
+ }
46519
+ }
46520
+ if (status === 400) {
46521
+ if (lower.includes("model not found") || lower.includes("not registered") || lower.includes("does not exist") || lower.includes("unknown model") || lower.includes("unsupported model") || lower.includes("no healthy deployment") || lower.includes("requires a google cloud project") || lower.includes("unsupported_client")) {
46522
+ return true;
46523
+ }
46524
+ if (provider?.toLowerCase().includes("antigravity") && lower.includes("invalid argument")) {
46525
+ return true;
46526
+ }
46527
+ if (isProvider(provider, "opencodezen") && (lower.includes("upstream request failed") || lower.includes("error from provider ("))) {
46528
+ return true;
46529
+ }
46530
+ }
46531
+ if (status === 500) {
46532
+ if (lower.includes("insufficient balance") || lower.includes("insufficient credit") || lower.includes("quota exceeded") || lower.includes("billing")) {
46533
+ return true;
46534
+ }
46535
+ }
46536
+ return false;
46537
+ }
46538
+ function exhaustedChainStatus(errors) {
46539
+ if (errors.length === 0)
46540
+ return 400;
46541
+ const isTransient = (e) => {
46542
+ if (e.status === 429 || e.status === 503)
46543
+ return true;
46544
+ if (hasQuotaExhaustionWording(e.message))
46545
+ return true;
46546
+ const upstream = e.status === 400 ? extractUpstreamStatus(e.message) : undefined;
46547
+ return upstream === 429 || upstream === 503;
46548
+ };
46549
+ return errors.every(isTransient) ? 503 : 400;
46550
+ }
46551
+ function isProvider(provider, needle) {
46552
+ if (!provider)
46553
+ return false;
46554
+ return provider.toLowerCase().replace(/[^a-z0-9]/g, "").includes(needle);
46555
+ }
46556
+ function parseErrorMessage(body) {
46557
+ try {
46558
+ const parsed = JSON.parse(body);
46559
+ if (typeof parsed.error === "string")
46560
+ return parsed.error;
46561
+ if (typeof parsed.error?.message === "string")
46562
+ return parsed.error.message;
46563
+ if (typeof parsed.message === "string")
46564
+ return parsed.message;
46565
+ } catch {}
46566
+ return body;
46567
+ }
46568
+ function truncate(s, max) {
46569
+ return s.length > max ? `${s.slice(0, max)}...` : s;
46570
+ }
46571
+ var init_fallback_handler = __esm(() => {
46572
+ init_logger();
46573
+ init_composed_handler();
46574
+ init_anthropic_error();
46575
+ init_quota_exhaustion();
45347
46576
  });
45348
46577
 
45349
46578
  // src/handlers/shared/thinking-signature.ts
@@ -45375,36 +46604,6 @@ function stripUnsignedThinkingBlocks(messages) {
45375
46604
  }
45376
46605
 
45377
46606
  // src/handlers/native-handler.ts
45378
- async function resolveAdvisorKeys() {
45379
- const keyFromAuthority = async (name) => {
45380
- try {
45381
- const auth = await credentials.getRequestAuth(name, { model: "" });
45382
- const k = auth.headers.Authorization?.replace(/^Bearer\s+/i, "") || auth.headers["x-api-key"];
45383
- return k || undefined;
45384
- } catch {
45385
- return;
45386
- }
45387
- };
45388
- const geminiKey = async () => {
45389
- const local = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || getApiKey("GEMINI_API_KEY");
45390
- if (local)
45391
- return local;
45392
- if (hasOpSources()) {
45393
- const r = await resolveOpKeyForEnvVars(new Set(["GEMINI_API_KEY", "GOOGLE_API_KEY"]), {
45394
- onAuthFailure: "skip"
45395
- });
45396
- return r.GEMINI_API_KEY || r.GOOGLE_API_KEY || undefined;
45397
- }
45398
- return;
45399
- };
45400
- const [openrouter, google, openai] = await Promise.all([
45401
- keyFromAuthority("openrouter"),
45402
- geminiKey(),
45403
- keyFromAuthority("openai")
45404
- ]);
45405
- return { openrouter, google, openai };
45406
- }
45407
-
45408
46607
  class NativeHandler {
45409
46608
  apiKey;
45410
46609
  baseUrl;
@@ -45423,65 +46622,7 @@ class NativeHandler {
45423
46622
  if (strippedThinking > 0) {
45424
46623
  log(`[Native] stripped ${strippedThinking} unsigned thinking block(s) from history for ${target} (foreign-provider origin)`);
45425
46624
  }
45426
- const advisorCfg = loadAdvisorSwapConfig(this.advisorModels, this.advisorCollector);
45427
- let advisorSwapped = null;
45428
- let advisorRewrittenIds = [];
45429
- if (advisorCfg.enabled) {
45430
- advisorSwapped = swapAdvisorToolInBody(payload);
45431
- if (advisorSwapped) {
45432
- log("[Native][advisor-swap] replaced advisor_20260301 with regular tool 'advisor'");
45433
- logAdvisorEvent(advisorCfg, {
45434
- kind: "swap_applied",
45435
- model: target,
45436
- originalTool: advisorSwapped.originalTool,
45437
- regularTool: advisorSwapped.regularTool
45438
- });
45439
- }
45440
- if (advisorCfg.models && advisorCfg.models.length > 0) {
45441
- const pendingIds = findPendingAdvisorToolResults(payload);
45442
- if (pendingIds.length > 0) {
45443
- const adviceMap = new Map;
45444
- for (const id of pendingIds) {
45445
- const advisorKeys = await resolveAdvisorKeys();
45446
- const advice = await fetchMultiModelAdvice(id, payload.messages, advisorCfg.models, advisorCfg.collector ?? null, {
45447
- ...advisorKeys,
45448
- anthropic: originalHeaders["x-api-key"]
45449
- });
45450
- adviceMap.set(id, advice);
45451
- }
45452
- advisorRewrittenIds = rewriteAdvisorToolResults(payload, (id) => adviceMap.get(id) ?? stubAdvisorAdvice(id));
45453
- if (advisorRewrittenIds.length > 0) {
45454
- log(`[Native][advisor] rewrote ${advisorRewrittenIds.length} tool_result(s) with multi-model advice from [${advisorCfg.models.join(", ")}]${advisorCfg.collector ? ` (collector: ${advisorCfg.collector})` : " (no collector)"}`);
45455
- logAdvisorEvent(advisorCfg, {
45456
- kind: "multi_model_rewrite",
45457
- ids: advisorRewrittenIds,
45458
- models: advisorCfg.models,
45459
- collector: advisorCfg.collector,
45460
- model: target
45461
- });
45462
- }
45463
- }
45464
- } else {
45465
- advisorRewrittenIds = rewriteAdvisorToolResults(payload, stubAdvisorAdvice);
45466
- if (advisorRewrittenIds.length > 0) {
45467
- log(`[Native][advisor-swap] rewrote ${advisorRewrittenIds.length} error tool_result(s) with stub advice: ${advisorRewrittenIds.join(", ")}`);
45468
- logAdvisorEvent(advisorCfg, {
45469
- kind: "tool_result_rewritten",
45470
- ids: advisorRewrittenIds,
45471
- model: target
45472
- });
45473
- }
45474
- }
45475
- if (advisorCfg.dumpBodies) {
45476
- logAdvisorEvent(advisorCfg, {
45477
- kind: "request_body",
45478
- swapApplied: !!advisorSwapped,
45479
- rewrittenIds: advisorRewrittenIds,
45480
- model: target,
45481
- body: trimForLog(payload)
45482
- });
45483
- }
45484
- }
46625
+ const advisorSwapped = c.get(ADVISOR_SWAPPED_CONTEXT_KEY) === true;
45485
46626
  log(`
45486
46627
  === [NATIVE] Claude Code \u2192 Anthropic API Request ===`);
45487
46628
  log(`[Native] x-api-key: ${originalHeaders["x-api-key"] ? maskCredential(originalHeaders["x-api-key"]) : "(not set)"}`);
@@ -45515,7 +46656,7 @@ class NativeHandler {
45515
46656
  const { stripped, changed } = stripAdvisorBeta(incomingBeta);
45516
46657
  if (changed) {
45517
46658
  log(`[Native][advisor-swap] stripped advisor-tool beta; before=${incomingBeta} after=${stripped ?? "(empty)"}`);
45518
- logAdvisorEvent(advisorCfg, {
46659
+ logAdvisorEvent(loadAdvisorSwapConfig(this.advisorModels, this.advisorCollector), {
45519
46660
  kind: "beta_stripped",
45520
46661
  before: incomingBeta,
45521
46662
  after: stripped ?? ""
@@ -45552,7 +46693,6 @@ class NativeHandler {
45552
46693
  controller.enqueue(value);
45553
46694
  const chunkText = decoder.decode(value, { stream: true });
45554
46695
  buffer += chunkText;
45555
- recordAdvisorEventsFromChunk(advisorCfg, chunkText);
45556
46696
  const lines = buffer.split(`
45557
46697
  `);
45558
46698
  buffer = lines.pop() || "";
@@ -45582,11 +46722,6 @@ class NativeHandler {
45582
46722
  log(`
45583
46723
  === [NATIVE] Response ===`);
45584
46724
  log(JSON.stringify(data, null, 2));
45585
- if (advisorCfg.enabled) {
45586
- try {
45587
- recordAdvisorEventsFromChunk(advisorCfg, JSON.stringify(data));
45588
- } catch {}
45589
- }
45590
46725
  const responseHeaders = { "Content-Type": "application/json" };
45591
46726
  if (anthropicResponse.headers.has("anthropic-version")) {
45592
46727
  responseHeaders["anthropic-version"] = anthropicResponse.headers.get("anthropic-version");
@@ -45599,30 +46734,10 @@ class NativeHandler {
45599
46734
  }
45600
46735
  async shutdown() {}
45601
46736
  }
45602
- function trimForLog(payload) {
45603
- const TEXT_TRUNC = 400;
45604
- const clone = structuredClone(payload);
45605
- const trimStr = (s) => typeof s === "string" && s.length > TEXT_TRUNC ? `${s.slice(0, TEXT_TRUNC)}\u2026 [+${s.length - TEXT_TRUNC} chars]` : s;
45606
- const walk = (v) => {
45607
- if (typeof v === "string")
45608
- return trimStr(v);
45609
- if (Array.isArray(v))
45610
- return v.map(walk);
45611
- if (v && typeof v === "object") {
45612
- const out = {};
45613
- for (const [k, val] of Object.entries(v))
45614
- out[k] = walk(val);
45615
- return out;
45616
- }
45617
- return v;
45618
- };
45619
- return walk(clone);
45620
- }
45621
46737
  var init_native_handler = __esm(() => {
45622
46738
  init_authority();
45623
- init_op_source();
45624
46739
  init_logger();
45625
- init_profile_config();
46740
+ init_advisor_decorator();
45626
46741
  init_native_handler_advisor();
45627
46742
  init_anthropic_error();
45628
46743
  });
@@ -46463,6 +47578,10 @@ async function createProxyServer(port, _openrouterApiKey, model, monitorMode = f
46463
47578
  log(`[Proxy] behavior hooks load skipped: ${err instanceof Error ? err.message : String(err)}`);
46464
47579
  }
46465
47580
  const nativeHandler = new NativeHandler(anthropicApiKey, options.advisorModels, options.advisorCollector);
47581
+ const advisorPresence = createAdvisorPresenceMonitor();
47582
+ const withAdvisor = (handler, presence) => withAdvisorSwap(handler, loadAdvisorSwapConfig(options.advisorModels, options.advisorCollector), {
47583
+ presence
47584
+ });
46466
47585
  const requestShapingOpts = {
46467
47586
  effortOverride: isEffortLevel(options.effortOverride) ? options.effortOverride : undefined,
46468
47587
  modelParams: options.modelParams,
@@ -46872,9 +47991,9 @@ ${plan.hint}` : `[Route] ${plan.reason}`;
46872
47991
  if (!monitorMode && options.classifier?.enabled && isAutoModeClassifierRequest(body)) {
46873
47992
  log(`[Classifier] auto-mode permission classifier \u2192 native Anthropic (model ${body.model} \u2192 ${options.classifier.model})`);
46874
47993
  rewriteClassifierForNative(body, options.classifier.model);
46875
- return nativeHandler.handle(c, body);
47994
+ return await withAdvisor(nativeHandler).handle(c, body);
46876
47995
  }
46877
- const handler = await getHandlerForRequest(body.model);
47996
+ const handler = withAdvisor(await getHandlerForRequest(body.model), advisorPresence);
46878
47997
  return await handler.handle(c, body);
46879
47998
  } catch (e) {
46880
47999
  log(`[Proxy] Error: ${e}`);
@@ -46930,8 +48049,10 @@ var init_proxy_server = __esm(() => {
46930
48049
  init_authority();
46931
48050
  init_hooks();
46932
48051
  init_behavior();
48052
+ init_advisor_decorator();
46933
48053
  init_composed_handler();
46934
48054
  init_fallback_handler();
48055
+ init_native_handler_advisor();
46935
48056
  init_native_handler();
46936
48057
  init_anthropic_error();
46937
48058
  init_logger();
@@ -56287,7 +57408,11 @@ function parseAdvisorFlag(value) {
56287
57408
  } else {
56288
57409
  collector = collectorPart;
56289
57410
  }
56290
- return { models, collector };
57411
+ return {
57412
+ models,
57413
+ collector,
57414
+ collectorDefaulted: models.length > 1 && collectorPart === undefined
57415
+ };
56291
57416
  }
56292
57417
  async function parseArgs(args) {
56293
57418
  const config = {
@@ -56435,7 +57560,8 @@ async function parseArgs(args) {
56435
57560
  const parsed = parseAdvisorFlag(modelsArg);
56436
57561
  config.advisorModels = parsed.models;
56437
57562
  config.advisorCollector = parsed.collector;
56438
- config.monitor = true;
57563
+ config.advisorCollectorDefaulted = parsed.collectorDefaulted;
57564
+ config.advisor = true;
56439
57565
  } else if (arg === "--stdin") {
56440
57566
  config.stdin = true;
56441
57567
  } else if (arg === "--free") {
@@ -56669,10 +57795,10 @@ Usage: claudish --models --provider <slug>`);
56669
57795
  if (config._sawVerbose && !config.interactive && !config.claudeArgs.includes("--verbose") && !config.claudeArgs.includes("-v")) {
56670
57796
  config.claudeArgs.push("--verbose");
56671
57797
  }
57798
+ if ((config.monitor || config.advisor) && process.env.ANTHROPIC_API_KEY?.includes("placeholder")) {
57799
+ delete process.env.ANTHROPIC_API_KEY;
57800
+ }
56672
57801
  if (config.monitor) {
56673
- if (process.env.ANTHROPIC_API_KEY?.includes("placeholder")) {
56674
- delete process.env.ANTHROPIC_API_KEY;
56675
- }
56676
57802
  if (!config.quiet) {
56677
57803
  console.log("[claudish] Monitor mode enabled - proxying to real Anthropic API");
56678
57804
  console.log("[claudish] Using Claude Code's native authentication");
@@ -57639,7 +58765,7 @@ ${h("OPTIONS")}
57639
58765
  ${green("--stdin")} Read prompt from stdin (large prompts / piping)
57640
58766
  ${green("--free")} Show only FREE models in the interactive selector
57641
58767
  ${green("--monitor")} Monitor mode - proxy to REAL Anthropic API and log traffic
57642
- ${green("--advisor")} ${yellow('"m1,m2[:collector]"')} Multi-model advisor replacement (implies --monitor)
58768
+ ${green("--advisor")} ${yellow('"m1,m2[:collector]"')} Multi-model advisor replacement (works with any --model)
57643
58769
  ${green("--model-params")} ${yellow('"k=v,..."')} Extra request params merged into the payload (e.g. reasoning.mode=pro)
57644
58770
  ${green("--effort-override")} ${yellow("<level>")} Pin reasoning effort verbatim, skipping the per-model clamp
57645
58771
  ${green("--pro-on-ultracode")} Apply the model's catalog preset while in ultracode (opt-in)
@@ -65601,12 +66727,34 @@ function shouldHideIncidentalAnthropicKey(config, env = process.env) {
65601
66727
  return false;
65602
66728
  return !wantsAnthropicApiBilling(config, env);
65603
66729
  }
66730
+ function scrubInheritedClaudishPlaceholders(env) {
66731
+ const removed = [];
66732
+ for (const name of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]) {
66733
+ if (isClaudishPlaceholderCredential(name, env[name])) {
66734
+ delete env[name];
66735
+ removed.push(name);
66736
+ }
66737
+ }
66738
+ return { removed };
66739
+ }
66740
+ function isClaudishPlaceholderCredential(name, value) {
66741
+ if (name === "ANTHROPIC_API_KEY")
66742
+ return value === CLAUDISH_PLACEHOLDER_API_KEY;
66743
+ if (name === "ANTHROPIC_AUTH_TOKEN")
66744
+ return value === CLAUDISH_PLACEHOLDER_AUTH_TOKEN;
66745
+ return false;
66746
+ }
66747
+ function isRealAnthropicEnvCredential(env, name) {
66748
+ const value = env[name];
66749
+ return Boolean(value) && !isClaudishPlaceholderCredential(name, value);
66750
+ }
65604
66751
  function hasResolvableAnthropicAuth(deps = {}) {
65605
66752
  const env = deps.env ?? process.env;
65606
66753
  const fileExists = deps.fileExists ?? existsSync31;
65607
66754
  const keychainProbe = deps.keychainProbe ?? defaultKeychainAnthropicProbe;
65608
- if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN)
66755
+ if (isRealAnthropicEnvCredential(env, "ANTHROPIC_API_KEY") || isRealAnthropicEnvCredential(env, "ANTHROPIC_AUTH_TOKEN")) {
65609
66756
  return true;
66757
+ }
65610
66758
  if (fileExists(join41(homedir36(), ".claude", ".credentials.json")))
65611
66759
  return true;
65612
66760
  return keychainProbe();
@@ -65614,8 +66762,11 @@ function hasResolvableAnthropicAuth(deps = {}) {
65614
66762
  function shouldPreserveNativeAuth(config) {
65615
66763
  return hasNativeAnthropicMapping(config) || classifierPassthroughEnabled(config) && hasResolvableAnthropicAuth();
65616
66764
  }
66765
+ function isAdvisorNativeSession(config) {
66766
+ return Boolean(config.advisor) && !config.model && !config.modelChain;
66767
+ }
65617
66768
  function isProxyAuthMode(config) {
65618
- return !config.monitor && !shouldPreserveNativeAuth(config);
66769
+ return !config.monitor && !isAdvisorNativeSession(config) && !shouldPreserveNativeAuth(config);
65619
66770
  }
65620
66771
  function managedSettingsPath() {
65621
66772
  if (isWindows2()) {
@@ -66012,9 +67163,48 @@ function resolveContextWindowEnv(realWindow, processEnv = process.env) {
66012
67163
  notice: `[claudish] Model's real context window (${realWindow.toLocaleString()}) is below ` + `Claude Code's ${MIN_AUTO_COMPACT_WINDOW.toLocaleString()}-token auto-compact floor \u2014 ` + "leaving CLAUDE_CODE_AUTO_COMPACT_WINDOW unset so native auto-compaction stays on."
66013
67164
  };
66014
67165
  }
67166
+ function resolveAdvisorToolEnv(config, processEnv = process.env) {
67167
+ if (!config.advisor)
67168
+ return { vars: {}, source: "off" };
67169
+ if (processEnv[ADVISOR_TOOL_ENV_VAR] !== undefined)
67170
+ return { vars: {}, source: "inherited" };
67171
+ return { vars: { [ADVISOR_TOOL_ENV_VAR]: "1" }, source: "claudish" };
67172
+ }
67173
+ function discoverUserAdvisorModel(claudeArgs = [], cwd = process.cwd()) {
67174
+ const sources = userSettingsFileCandidates(cwd).filter((file) => existsSync31(file));
67175
+ const idx = claudeArgs.indexOf("--settings");
67176
+ const settingsArg = idx === -1 ? undefined : claudeArgs[idx + 1];
67177
+ if (settingsArg)
67178
+ sources.push(settingsArg);
67179
+ const managed = managedSettingsPath();
67180
+ if (existsSync31(managed))
67181
+ sources.push(managed);
67182
+ let effective;
67183
+ for (const source of sources) {
67184
+ const layer = parseSettingsArgSafe(source);
67185
+ if (!layer || !("advisorModel" in layer))
67186
+ continue;
67187
+ const value = layer.advisorModel;
67188
+ effective = typeof value === "string" && value !== "" ? value : undefined;
67189
+ }
67190
+ return effective;
67191
+ }
67192
+ function resolveAdvisorModelArg(config, cwd = process.cwd()) {
67193
+ if (!config.advisor)
67194
+ return { args: [], source: "off" };
67195
+ const userChoice = discoverUserAdvisorModel(config.claudeArgs, cwd);
67196
+ if (userChoice)
67197
+ return { args: [], model: userChoice, source: "inherited" };
67198
+ return {
67199
+ args: ["--advisor", CLAUDISH_CHILD_ADVISOR_MODEL],
67200
+ model: CLAUDISH_CHILD_ADVISOR_MODEL,
67201
+ source: "claudish"
67202
+ };
67203
+ }
66015
67204
  async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66016
67205
  const hasProfileMappings = config.modelOpus || config.modelSonnet || config.modelHaiku || config.modelSubagent;
66017
- const modelId = config.model || (hasProfileMappings || config.monitor ? undefined : "unknown");
67206
+ const advisorNativeSession = isAdvisorNativeSession(config);
67207
+ const modelId = config.model || (hasProfileMappings || config.monitor || advisorNativeSession ? undefined : "unknown");
66018
67208
  const portMatch = proxyUrl.match(/:(\d+)/);
66019
67209
  const port = portMatch ? portMatch[1] : "unknown";
66020
67210
  const proxyAuthMode = isProxyAuthMode(config);
@@ -66026,6 +67216,7 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66026
67216
  return 1;
66027
67217
  }
66028
67218
  const userStatusLineCommand = discoverUserStatusLineCommand(config.claudeArgs);
67219
+ const advisorModelArg = resolveAdvisorModelArg(config);
66029
67220
  const {
66030
67221
  path: tempSettingsPath,
66031
67222
  statusLine,
@@ -66034,6 +67225,7 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66034
67225
  mergeUserSettingsIfPresent(config, tempSettingsPath, statusLine, proxyAuthMode);
66035
67226
  const claudeArgs = [];
66036
67227
  claudeArgs.push("--settings", tempSettingsPath);
67228
+ claudeArgs.push(...advisorModelArg.args);
66037
67229
  if (config.interactive) {
66038
67230
  if (config.autoApprove) {
66039
67231
  claudeArgs.push("--dangerously-skip-permissions");
@@ -66059,12 +67251,14 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66059
67251
  }
66060
67252
  const isLocalModel = modelId ? modelId.startsWith("ollama/") || modelId.startsWith("ollama:") || modelId.startsWith("lmstudio/") || modelId.startsWith("lmstudio:") || modelId.startsWith("vllm/") || modelId.startsWith("vllm:") || modelId.startsWith("mlx/") || modelId.startsWith("mlx:") || modelId.startsWith("http://") || modelId.startsWith("https://") : false;
66061
67253
  const modelDisplayName = modelId || config.profile || "default";
67254
+ const advisorToolEnv = resolveAdvisorToolEnv(config);
66062
67255
  const env = {
66063
67256
  ...process.env,
66064
67257
  ANTHROPIC_BASE_URL: proxyUrl,
66065
67258
  [ENV.CLAUDISH_ACTIVE_MODEL_NAME]: modelDisplayName,
66066
67259
  CLAUDISH_IS_LOCAL: isLocalModel ? "true" : "false",
66067
- [ENV.CLAUDISH_TOKEN_FILE]: tokenFilePath
67260
+ [ENV.CLAUDISH_TOKEN_FILE]: tokenFilePath,
67261
+ ...advisorToolEnv.vars
66068
67262
  };
66069
67263
  if (modelId) {
66070
67264
  const parsedSpec = parseModelSpec(modelId);
@@ -66073,9 +67267,15 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66073
67267
  env[ENV.CLAUDISH_PROVIDER_NAME] = providerDisplayName;
66074
67268
  }
66075
67269
  }
67270
+ if (advisorToolEnv.source !== "off") {
67271
+ log(`[claude-runner] ${ADVISOR_TOOL_ENV_VAR}=${env[ADVISOR_TOOL_ENV_VAR]} (${advisorToolEnv.source})`);
67272
+ }
67273
+ if (advisorModelArg.source !== "off") {
67274
+ log(`[claude-runner] child advisor model=${advisorModelArg.model} (${advisorModelArg.source}` + `${advisorModelArg.source === "inherited" ? "; user setting kept, no --advisor passed" : " via --advisor"})`);
67275
+ }
66076
67276
  let hidAnthropicApiKey = false;
66077
67277
  delete env.CLAUDECODE;
66078
- if (config.monitor) {
67278
+ if (config.monitor || advisorNativeSession) {
66079
67279
  delete env.ANTHROPIC_API_KEY;
66080
67280
  delete env.ANTHROPIC_AUTH_TOKEN;
66081
67281
  if (modelId) {
@@ -66088,7 +67288,11 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66088
67288
  env[ENV.ANTHROPIC_SMALL_FAST_MODEL] = modelId;
66089
67289
  }
66090
67290
  if (shouldPreserveNativeAuth(config)) {
66091
- if (shouldHideIncidentalAnthropicKey(config)) {
67291
+ const scrubbed = scrubInheritedClaudishPlaceholders(env);
67292
+ if (scrubbed.removed.length > 0) {
67293
+ log(`[claude-runner] Removed inherited claudish placeholder credentials: ${scrubbed.removed.join(", ")}`);
67294
+ }
67295
+ if (shouldHideIncidentalAnthropicKey(config, env)) {
66092
67296
  delete env.ANTHROPIC_API_KEY;
66093
67297
  hidAnthropicApiKey = true;
66094
67298
  }
@@ -66096,8 +67300,8 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66096
67300
  if (classifierPassthroughEnabled(config)) {
66097
67301
  console.error("[claudish] classifier passthrough enabled but no Anthropic credentials detected \u2014 " + "classifier requests will fail to authenticate against api.anthropic.com. " + "Map a role to a native Claude model (e.g. --model-sonnet claude-sonnet-5) or set ANTHROPIC_API_KEY.");
66098
67302
  }
66099
- env.ANTHROPIC_API_KEY = "sk-ant-api03-placeholder-not-used-proxy-handles-auth-with-openrouter-key-xxxxxxxxxxxxxxxxxxxxx";
66100
- env.ANTHROPIC_AUTH_TOKEN = "placeholder-token-not-used-proxy-handles-auth";
67303
+ env.ANTHROPIC_API_KEY = CLAUDISH_PLACEHOLDER_API_KEY;
67304
+ env.ANTHROPIC_AUTH_TOKEN = CLAUDISH_PLACEHOLDER_AUTH_TOKEN;
66101
67305
  const realWindow = await computeMainThreadContextWindow(config);
66102
67306
  const contextEnv = resolveContextWindowEnv(realWindow, process.env);
66103
67307
  Object.assign(env, contextEnv.vars);
@@ -66311,7 +67515,7 @@ var restoreTerminal = null, macosKeychainAnthropicResult, defaultKeychainAnthrop
66311
67515
  macosKeychainAnthropicResult = false;
66312
67516
  }
66313
67517
  return macosKeychainAnthropicResult;
66314
- }, STALE_TOKEN_FILE_MS, MAX_TOKEN_FILES_SCANNED = 4000, CLAUDE_CODE_DEFAULT_MAX_CONTEXT = 200000, USER_STATUS_LINE_TIMEOUT_SECONDS = 3, MIN_AUTO_COMPACT_WINDOW = 200000, SIGNAL_EXIT_NUMBERS;
67518
+ }, CLAUDISH_PLACEHOLDER_API_KEY = "sk-ant-api03-placeholder-not-used-proxy-handles-auth-with-openrouter-key-xxxxxxxxxxxxxxxxxxxxx", CLAUDISH_PLACEHOLDER_AUTH_TOKEN = "placeholder-token-not-used-proxy-handles-auth", STALE_TOKEN_FILE_MS, MAX_TOKEN_FILES_SCANNED = 4000, CLAUDE_CODE_DEFAULT_MAX_CONTEXT = 200000, USER_STATUS_LINE_TIMEOUT_SECONDS = 3, MIN_AUTO_COMPACT_WINDOW = 200000, ADVISOR_TOOL_ENV_VAR = "CLAUDE_CODE_ENABLE_EXPERIMENTAL_ADVISOR_TOOL", CLAUDISH_CHILD_ADVISOR_MODEL = "sonnet", SIGNAL_EXIT_NUMBERS;
66315
67519
  var init_claude_runner = __esm(() => {
66316
67520
  init_model_catalog();
66317
67521
  init_config2();
@@ -66596,6 +67800,226 @@ var init_catalog_warm = __esm(() => {
66596
67800
  ];
66597
67801
  });
66598
67802
 
67803
+ // src/advisor-startup.ts
67804
+ function isClaudeCodeBoolTrue(value) {
67805
+ if (value === undefined)
67806
+ return false;
67807
+ const v = value.trim().toLowerCase();
67808
+ return v === "1" || v === "true" || v === "yes" || v === "on";
67809
+ }
67810
+ function routeAdvisorModel(modelSpec, role) {
67811
+ return advisorRouteFor(modelSpec, role);
67812
+ }
67813
+ function advisorCredentialEnvName(credential) {
67814
+ if (credential === "anthropic")
67815
+ return "ANTHROPIC_API_KEY";
67816
+ return getProviderByName(ADVISOR_AUTHORITY_PROVIDER[credential])?.apiKeyEnvVar || `a ${credential} API key`;
67817
+ }
67818
+ function alsoAcceptedEnvName(credential) {
67819
+ return credential === "google" ? "GOOGLE_API_KEY" : null;
67820
+ }
67821
+ async function resolveAdvisorCredentials(needed) {
67822
+ const present = async (credential) => Boolean(await resolveAdvisorCredential(credential));
67823
+ const list = [...needed];
67824
+ const results = await Promise.all(list.map(present));
67825
+ const out = {};
67826
+ list.forEach((c, i) => {
67827
+ out[c] = results[i];
67828
+ });
67829
+ return out;
67830
+ }
67831
+ function advisorModelStatus(model, route, presence) {
67832
+ const credentialName = advisorCredentialEnvName(route.credential);
67833
+ if (route.unresolvedAlias) {
67834
+ return {
67835
+ model,
67836
+ route,
67837
+ callable: false,
67838
+ credentialName,
67839
+ unresolved: `${model} is a claudish alias, not a model id ${route.host} accepts, and the model ` + "catalog holds no id for it (it is cold or has never been fetched), so claudish would " + `have to POST "${route.unresolvedAlias}" verbatim`
67840
+ };
67841
+ }
67842
+ return {
67843
+ model,
67844
+ route,
67845
+ callable: presence[route.credential] === true,
67846
+ credentialName
67847
+ };
67848
+ }
67849
+ function describeMissingCredential2(s) {
67850
+ const also = alsoAcceptedEnvName(s.route.credential);
67851
+ return `${s.model} calls ${s.route.host} and needs ${s.credentialName}${also ? ` (or ${also})` : ""}; ` + "none found in env, config, keychain or 1Password";
67852
+ }
67853
+ function describeUncallable(s) {
67854
+ return s.unresolved ?? describeMissingCredential2(s);
67855
+ }
67856
+ function formatCarriesTools(format) {
67857
+ try {
67858
+ const out = format.convertTools({
67859
+ tools: [
67860
+ {
67861
+ name: "advisor",
67862
+ description: "startup probe",
67863
+ input_schema: { type: "object", properties: {}, additionalProperties: false }
67864
+ }
67865
+ ]
67866
+ }, false);
67867
+ return Array.isArray(out) && out.length > 0;
67868
+ } catch {
67869
+ return true;
67870
+ }
67871
+ }
67872
+ function pinnedFormatForTransport(transport, modelName) {
67873
+ switch (transport) {
67874
+ case "ollamacloud":
67875
+ return new OllamaAPIFormat(modelName);
67876
+ default:
67877
+ return null;
67878
+ }
67879
+ }
67880
+ function resolveMainModelFact(model) {
67881
+ const native = nativeRouteFor(model);
67882
+ if (native)
67883
+ return { model, providerName: native.displayName, carriesTools: true };
67884
+ const resolution = resolveModelProvider(model);
67885
+ const definitionName = resolution.catalogName === "gemini" ? "google" : resolution.catalogName;
67886
+ const definition = definitionName ? getProviderByName(definitionName) : undefined;
67887
+ const format = definition ? pinnedFormatForTransport(definition.transport, resolution.modelName) : null;
67888
+ const providerName = resolution.category === "unknown" ? "unresolved (routed on the first request)" : definition?.displayName || resolution.providerName;
67889
+ return {
67890
+ model,
67891
+ providerName,
67892
+ carriesTools: format ? formatCarriesTools(format) : true,
67893
+ wireFormat: format?.getName()
67894
+ };
67895
+ }
67896
+ function mainModelSpecs(config) {
67897
+ if (config.modelChain && config.modelChain.length > 0)
67898
+ return [...config.modelChain];
67899
+ if (config.model)
67900
+ return [config.model];
67901
+ const tiers = [config.modelOpus, config.modelSonnet, config.modelHaiku, config.modelSubagent];
67902
+ return [...new Set(tiers.filter((m) => typeof m === "string" && m.length > 0))];
67903
+ }
67904
+ function modelIdentity(spec) {
67905
+ const model = parseModelSpec(spec).model.toLowerCase();
67906
+ const slash = model.lastIndexOf("/");
67907
+ return slash >= 0 ? model.slice(slash + 1) : model;
67908
+ }
67909
+ function panelMembersMatchingMain(panel, mainModels) {
67910
+ const main = new Set(mainModels.map(modelIdentity));
67911
+ return panel.filter((p) => main.has(modelIdentity(p)));
67912
+ }
67913
+ function refusalBeforeCredentials(facts) {
67914
+ const disable = facts.childEnv[ADVISOR_DISABLE_ENV_VAR];
67915
+ if (isClaudeCodeBoolTrue(disable)) {
67916
+ return `${ADVISOR_DISABLE_ENV_VAR}=${JSON.stringify(disable)} is set in your environment. ` + `Claude Code then never offers the advisor tool, and ${ADVISOR_TOOL_ENV_VAR} does not ` + `override it. Unset ${ADVISOR_DISABLE_ENV_VAR} to use --advisor.`;
67917
+ }
67918
+ const toolless = facts.mainModels.filter((m) => !m.carriesTools);
67919
+ if (toolless.length > 0) {
67920
+ const named = toolless.map((m) => `${m.model} (${m.providerName}${m.wireFormat ? `, ${m.wireFormat}` : ""})`).join(", ");
67921
+ return `the main model path cannot carry tools: ${named} drops every tool from the request, ` + "so the advisor tool can never reach the model. Choose a main model on a provider " + "that supports tool calls, or drop --advisor.";
67922
+ }
67923
+ if (facts.panel.length === 0) {
67924
+ return 'no advisor panel models were given. Use --advisor "model1[,model2][:collector]".';
67925
+ }
67926
+ return null;
67927
+ }
67928
+ function describeRoute(s) {
67929
+ return `${s.model} -> ${s.route.host} (${s.credentialName}, billed per token)`;
67930
+ }
67931
+ function decideAdvisorStartup(facts) {
67932
+ const early = refusalBeforeCredentials(facts);
67933
+ if (early)
67934
+ return { kind: "refuse", reason: early };
67935
+ const missingPanel = facts.panelStatus.filter((s) => !s.callable);
67936
+ if (missingPanel.length > 0) {
67937
+ return {
67938
+ kind: "refuse",
67939
+ reason: `advisor panel model${missingPanel.length > 1 ? "s" : ""} cannot be called: ` + `${missingPanel.map(describeUncallable).join("; ")}. ` + "Set the key, or remove the model from --advisor."
67940
+ };
67941
+ }
67942
+ let effectiveCollector = facts.collector;
67943
+ let collectorLine = "none";
67944
+ const cs = facts.collectorStatus;
67945
+ if (cs && !cs.callable) {
67946
+ if (!facts.collectorDefaulted) {
67947
+ return {
67948
+ kind: "refuse",
67949
+ reason: `collector ${describeUncallable(cs)}. ` + 'Set the key, name another collector ("a,b:collector"), or end the value with ":" for no collector.'
67950
+ };
67951
+ }
67952
+ effectiveCollector = null;
67953
+ collectorLine = cs.unresolved ? `none \u2014 ${cs.unresolved}; panel answers will be concatenated` : `none \u2014 the default collector ${cs.model} needs ${cs.credentialName}, which was not ` + "found; panel answers will be concatenated";
67954
+ } else if (cs) {
67955
+ collectorLine = describeRoute(cs);
67956
+ }
67957
+ const notice = ["[claudish] --advisor is on for this launch"];
67958
+ notice.push(" panel (panel calls never use a subscription):");
67959
+ for (const s of facts.panelStatus)
67960
+ notice.push(` ${describeRoute(s)}`);
67961
+ notice.push(` collector: ${collectorLine}`);
67962
+ const main = facts.mainModels.length === 0 ? "your Claude Code session" : facts.mainModels.map((m) => `${m.model} via ${m.providerName}`).join(" -> ");
67963
+ notice.push(` main model: ${main}`);
67964
+ const envValue = facts.childEnv[ADVISOR_TOOL_ENV_VAR];
67965
+ if (facts.toolEnv.source === "inherited") {
67966
+ notice.push(` ${ADVISOR_TOOL_ENV_VAR}=${JSON.stringify(envValue ?? "")} inherited from your environment` + (isClaudeCodeBoolTrue(envValue) ? "" : " \u2014 Claude Code reads this value as FALSE, so the advisor tool appears only if your main model qualifies for it without the override"));
67967
+ } else {
67968
+ notice.push(` ${ADVISOR_TOOL_ENV_VAR}=1 set by claudish`);
67969
+ }
67970
+ const includesMain = panelMembersMatchingMain(facts.panel, facts.mainModels.map((m) => m.model));
67971
+ if (includesMain.length > 0) {
67972
+ notice.push(` note: the panel includes the main model (${includesMain.join(", ")})`);
67973
+ }
67974
+ notice.push(" cost: each advisor call sends the FULL conversation to every panel model, uncached; " + "cost grows with panel size and transcript length");
67975
+ return { kind: "proceed", notice, effectiveCollector };
67976
+ }
67977
+ async function evaluateAdvisorStartup(config, toolEnv, childEnv = process.env, deps = { resolveCredentials: resolveAdvisorCredentials }) {
67978
+ if (!config.advisor)
67979
+ return null;
67980
+ const panel = config.advisorModels ?? [];
67981
+ const collector = config.advisorCollector ?? null;
67982
+ const mainModels = mainModelSpecs(config).map(resolveMainModelFact);
67983
+ const early = refusalBeforeCredentials({ childEnv, mainModels, panel });
67984
+ if (early)
67985
+ return { kind: "refuse", reason: early };
67986
+ let panelRoutes;
67987
+ let collectorRoute;
67988
+ try {
67989
+ panelRoutes = panel.map((m) => routeAdvisorModel(m, "panel"));
67990
+ collectorRoute = collector ? routeAdvisorModel(collector, "collector") : null;
67991
+ } catch (err) {
67992
+ return {
67993
+ kind: "refuse",
67994
+ reason: err instanceof Error ? err.message : String(err)
67995
+ };
67996
+ }
67997
+ const needed = new Set(panelRoutes.map((r) => r.credential));
67998
+ if (collectorRoute)
67999
+ needed.add(collectorRoute.credential);
68000
+ const presence = await deps.resolveCredentials(needed);
68001
+ return decideAdvisorStartup({
68002
+ panel,
68003
+ collector,
68004
+ collectorDefaulted: config.advisorCollectorDefaulted === true,
68005
+ mainModels,
68006
+ childEnv,
68007
+ toolEnv,
68008
+ panelStatus: panel.map((m, i) => advisorModelStatus(m, panelRoutes[i], presence)),
68009
+ collectorStatus: collector && collectorRoute ? advisorModelStatus(collector, collectorRoute, presence) : null
68010
+ });
68011
+ }
68012
+ var ADVISOR_DISABLE_ENV_VAR = "CLAUDE_CODE_DISABLE_ADVISOR_TOOL";
68013
+ var init_advisor_startup = __esm(() => {
68014
+ init_ollama_api_format();
68015
+ init_claude_runner();
68016
+ init_native_handler_advisor();
68017
+ init_model_parser();
68018
+ init_native_route();
68019
+ init_provider_definitions();
68020
+ init_provider_resolver();
68021
+ });
68022
+
66599
68023
  // src/tui/viz/text.ts
66600
68024
  function columns(n, fn, arg = "width") {
66601
68025
  if (!Number.isFinite(n))
@@ -69482,11 +70906,12 @@ Team Status`);
69482
70906
  process.exit(1);
69483
70907
  }
69484
70908
  const hasProfileTiers = cliConfig.modelOpus || cliConfig.modelSonnet || cliConfig.modelHaiku || cliConfig.modelSubagent;
69485
- if (cliConfig.interactive && !cliConfig.monitor && !cliConfig.model && !hasProfileTiers) {
70909
+ const advisorNativeSession = isAdvisorNativeSession(cliConfig);
70910
+ if (cliConfig.interactive && !cliConfig.monitor && !advisorNativeSession && !cliConfig.model && !hasProfileTiers) {
69486
70911
  cliConfig.model = await traceSpan("startup:model-select", () => selectModel({ freeOnly: cliConfig.freeOnly }).catch(handlePromptExit), { mayIncludeUserPrompt: true });
69487
70912
  console.log("");
69488
70913
  }
69489
- if (!cliConfig.interactive && !cliConfig.monitor && !cliConfig.model && !hasProfileTiers) {
70914
+ if (!cliConfig.interactive && !cliConfig.monitor && !advisorNativeSession && !cliConfig.model && !hasProfileTiers) {
69490
70915
  console.error("Error: Model must be specified in non-interactive mode");
69491
70916
  console.error("Use --model <model> flag, set CLAUDISH_MODEL env var, or use --profile");
69492
70917
  console.error("Try: claudish --models");
@@ -69546,16 +70971,36 @@ Team Status`);
69546
70971
  }
69547
70972
  }
69548
70973
  }
70974
+ const warmOutcome = await traceSpan("startup:catalog-warm", () => warmCatalogIfNeeded(cliConfig));
70975
+ if (warmOutcome === "hard_fail") {
70976
+ process.exit(1);
70977
+ }
70978
+ if (cliConfig.advisor) {
70979
+ await Promise.resolve().then(() => init_advisor_startup());
70980
+ const decision = await traceSpan("startup:advisor-check", () => evaluateAdvisorStartup(cliConfig, resolveAdvisorToolEnv(cliConfig)));
70981
+ if (decision?.kind === "refuse") {
70982
+ process.stderr.write(`[claudish] Error: --advisor cannot work in this launch: ${decision.reason}
70983
+ `);
70984
+ process.exit(1);
70985
+ }
70986
+ if (decision?.kind === "proceed") {
70987
+ cliConfig.advisorCollector = decision.effectiveCollector;
70988
+ await Promise.resolve().then(() => init_catalog_client());
70989
+ if (getCatalogEntries() === null) {
70990
+ const panel = cliConfig.advisorModels ?? [];
70991
+ decision.notice.push(` WARNING: the model catalog is not loaded (catalog warm: ${warmOutcome}), so claudish could not verify that ${panel.length === 1 ? "this panel model is" : "these panel models are"} routable: ${panel.join(", ")}. An unroutable one fails on its first advisor call instead of being refused here. Run \`claudish --models-refresh\` to check them at launch.`);
70992
+ }
70993
+ process.stderr.write(`${decision.notice.join(`
70994
+ `)}
70995
+ `);
70996
+ }
70997
+ }
69549
70998
  if (cliConfig.stdin) {
69550
70999
  const stdinInput = await traceSpan("startup:stdin-read", () => readStdin());
69551
71000
  if (stdinInput.trim()) {
69552
71001
  cliConfig.claudeArgs = [stdinInput, ...cliConfig.claudeArgs];
69553
71002
  }
69554
71003
  }
69555
- const warmOutcome = await traceSpan("startup:catalog-warm", () => warmCatalogIfNeeded(cliConfig));
69556
- if (warmOutcome === "hard_fail") {
69557
- process.exit(1);
69558
- }
69559
71004
  const port = cliConfig.port || await traceSpan("startup:find-port", () => findAvailablePort(DEFAULT_PORT_RANGE.start, DEFAULT_PORT_RANGE.end));
69560
71005
  const explicitModel = typeof cliConfig.model === "string" ? cliConfig.model : undefined;
69561
71006
  const modelMap = {