claudish 9.2.1 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1870 -460
  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.0";
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
@@ -44812,166 +44819,6 @@ ${text}`;
44812
44819
  };
44813
44820
  });
44814
44821
 
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
44822
  // src/handlers/native-handler-advisor.ts
44976
44823
  import { appendFileSync as appendFileSync7 } from "fs";
44977
44824
  function loadAdvisorSwapConfig(cliModels, cliCollector) {
@@ -45022,17 +44869,65 @@ function stripAdvisorBeta(betaHeader) {
45022
44869
  changed: true
45023
44870
  };
45024
44871
  }
44872
+ function scrubSecrets(text) {
44873
+ 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);
44874
+ }
44875
+ function sanitizeAdvisorReason(text, secretValues = []) {
44876
+ let out = text;
44877
+ for (const secret of secretValues) {
44878
+ const value = secret?.trim();
44879
+ if (!value || value.length < MIN_REDACTABLE_SECRET_LENGTH)
44880
+ continue;
44881
+ out = out.split(value).join(REDACTED);
44882
+ }
44883
+ return scrubSecrets(out);
44884
+ }
44885
+ function credentialValuesInHeaders(headers) {
44886
+ const values = [];
44887
+ const bearer = headers.Authorization ?? headers.authorization;
44888
+ if (bearer)
44889
+ values.push(bearer.replace(/^Bearer\s+/i, "").trim());
44890
+ const apiKey = headers["x-api-key"];
44891
+ if (apiKey)
44892
+ values.push(apiKey.trim());
44893
+ return values;
44894
+ }
44895
+ function credentialValuesOf(apiKeys) {
44896
+ return Object.values(apiKeys).filter((v) => typeof v === "string");
44897
+ }
45025
44898
  function logAdvisorEvent(cfg, event) {
44899
+ const ts = new Date().toISOString();
44900
+ const record = { ts, ...event };
44901
+ let line;
44902
+ try {
44903
+ line = scrubSecrets(JSON.stringify(record));
44904
+ } catch {
44905
+ return;
44906
+ }
44907
+ if (typeof event.kind === "string" && ORIGIN_RECORD_KINDS.has(event.kind)) {
44908
+ if (getLogFilePath() !== null)
44909
+ log(`${ADVISOR_ORIGIN_LOG_PREFIX} ${line}`);
44910
+ appendLine(getAlwaysOnLogPath(), `[${ts}] ${ADVISOR_ORIGIN_LOG_PREFIX} ${line}`);
44911
+ }
44912
+ appendLine(cfg.logPath, line);
44913
+ }
44914
+ function appendLine(path, line) {
44915
+ if (!path)
44916
+ return;
44917
+ try {
44918
+ appendFileSync7(path, `${line}
44919
+ `);
44920
+ } catch {}
44921
+ }
44922
+ function recordAdvisorEventsFromResponseBody(cfg, body, sessionId) {
44923
+ collectAdvisorIdsFromValue(body, 0, sessionId);
45026
44924
  if (!cfg.logPath)
45027
44925
  return;
45028
- const line = `${JSON.stringify({ ts: new Date().toISOString(), ...event })}
45029
- `;
45030
44926
  try {
45031
- appendFileSync7(cfg.logPath, line);
44927
+ logAdvisorMarkers(cfg, JSON.stringify(body));
45032
44928
  } catch {}
45033
44929
  }
45034
- function recordAdvisorEventsFromChunk(cfg, chunkText) {
45035
- extractAdvisorToolUseIds(chunkText);
44930
+ function logAdvisorMarkers(cfg, text) {
45036
44931
  if (!cfg.logPath)
45037
44932
  return;
45038
44933
  const markers = [
@@ -45046,37 +44941,275 @@ function recordAdvisorEventsFromChunk(cfg, chunkText) {
45046
44941
  for (const [needle, kind] of markers) {
45047
44942
  let i = 0;
45048
44943
  while (true) {
45049
- i = chunkText.indexOf(needle, i);
44944
+ i = text.indexOf(needle, i);
45050
44945
  if (i < 0)
45051
44946
  break;
45052
- const ctx = chunkText.slice(Math.max(0, i - 40), i + 160);
44947
+ const ctx = text.slice(Math.max(0, i - 40), i + 160);
45053
44948
  logAdvisorEvent(cfg, { kind, needle, ctx });
45054
44949
  i += needle.length;
45055
44950
  }
45056
44951
  }
45057
44952
  }
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]);
44953
+ function warnAdvisor(message) {
44954
+ logStderr(`[advisor] ${message}`);
44955
+ }
44956
+ function sessionKeyFor(sessionId) {
44957
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : NO_SESSION_BUCKET;
44958
+ }
44959
+ function isCallInFlight(toolUseId) {
44960
+ return (inFlightByToolUseId.get(toolUseId) ?? 0) > 0;
44961
+ }
44962
+ function bucketHasInFlightCall(bucket) {
44963
+ for (const id of bucket.keys())
44964
+ if (isCallInFlight(id))
44965
+ return true;
44966
+ return false;
44967
+ }
44968
+ function isExpired(call, now) {
44969
+ if (isCallInFlight(call.toolUseId))
44970
+ return false;
44971
+ return now - call.lastSeenAt > ADVISOR_PENDING_LIMITS.ttlMs;
44972
+ }
44973
+ function sweepExpiredIfDue() {
44974
+ const now = clock();
44975
+ if (now - lastSweepAt < SWEEP_INTERVAL_MS)
44976
+ return;
44977
+ lastSweepAt = now;
44978
+ for (const [key, bucket] of pendingBySession) {
44979
+ for (const [id, call] of bucket) {
44980
+ if (isExpired(call, now))
44981
+ bucket.delete(id);
44982
+ }
44983
+ if (bucket.size === 0)
44984
+ pendingBySession.delete(key);
44985
+ }
44986
+ }
44987
+ function touchBucket(key) {
44988
+ const bucket = pendingBySession.get(key);
44989
+ if (!bucket)
44990
+ return;
44991
+ pendingBySession.delete(key);
44992
+ pendingBySession.set(key, bucket);
44993
+ return bucket;
44994
+ }
44995
+ function bucketForWrite(key) {
44996
+ const existing = touchBucket(key);
44997
+ if (existing)
44998
+ return existing;
44999
+ const bucket = new Map;
45000
+ pendingBySession.set(key, bucket);
45001
+ while (pendingBySession.size > ADVISOR_PENDING_LIMITS.maxSessions) {
45002
+ const evictable = [...pendingBySession].find(([k, b]) => k !== key && !bucketHasInFlightCall(b));
45003
+ if (!evictable)
45004
+ break;
45005
+ pendingBySession.delete(evictable[0]);
45006
+ }
45007
+ return bucket;
45008
+ }
45009
+ function putCall(bucket, call) {
45010
+ bucket.delete(call.toolUseId);
45011
+ if (bucket.size >= ADVISOR_PENDING_LIMITS.maxCallsPerSession) {
45012
+ const oldest = [...bucket.keys()].find((id) => !isCallInFlight(id));
45013
+ if (oldest !== undefined)
45014
+ bucket.delete(oldest);
45015
+ }
45016
+ bucket.set(call.toolUseId, call);
45017
+ }
45018
+ function rememberAdvisorToolUseId(id, sessionId) {
45019
+ sweepExpiredIfDue();
45020
+ const key = sessionKeyFor(sessionId);
45021
+ const bucket = bucketForWrite(key);
45022
+ const now = clock();
45023
+ const existing = bucket.get(id);
45024
+ if (existing) {
45025
+ existing.lastSeenAt = now;
45026
+ putCall(bucket, existing);
45027
+ return;
45028
+ }
45029
+ putCall(bucket, { toolUseId: id, sessionKey: key, recordedAt: now, lastSeenAt: now });
45030
+ log(`[advisor] recorded advisor tool_use ${id} (session=${key})`);
45031
+ }
45032
+ function lookupAdvisorCall(toolUseId, sessionId) {
45033
+ sweepExpiredIfDue();
45034
+ const now = clock();
45035
+ const own = sessionKeyFor(sessionId);
45036
+ const keys = own === NO_SESSION_BUCKET ? [NO_SESSION_BUCKET] : [own, NO_SESSION_BUCKET];
45037
+ for (const key of keys) {
45038
+ const bucket = pendingBySession.get(key);
45039
+ const call = bucket?.get(toolUseId);
45040
+ if (!bucket || !call)
45041
+ continue;
45042
+ if (isExpired(call, now)) {
45043
+ bucket.delete(toolUseId);
45044
+ continue;
45045
+ }
45046
+ call.lastSeenAt = now;
45047
+ putCall(bucket, call);
45048
+ touchBucket(key);
45049
+ return call;
45050
+ }
45051
+ return;
45052
+ }
45053
+ function getAdvisorCall(toolUseId, sessionId) {
45054
+ return lookupAdvisorCall(toolUseId, sessionId);
45055
+ }
45056
+ function markAdvisorCallConsumed(toolUseId, result, sessionId) {
45057
+ const call = lookupAdvisorCall(toolUseId, sessionId);
45058
+ if (!call)
45059
+ return false;
45060
+ const own = sessionKeyFor(sessionId);
45061
+ if (call.sessionKey === NO_SESSION_BUCKET && own !== NO_SESSION_BUCKET) {
45062
+ pendingBySession.get(NO_SESSION_BUCKET)?.delete(toolUseId);
45063
+ call.sessionKey = own;
45064
+ putCall(bucketForWrite(own), call);
45065
+ }
45066
+ call.consumedAt ??= clock();
45067
+ call.result = result;
45068
+ return true;
45069
+ }
45070
+ function joinOrStartAdvisorCall(toolUseId, sessionId, start) {
45071
+ const key = `${sessionKeyFor(sessionId)}\x00${toolUseId}`;
45072
+ const existing = inFlightAdvisorCalls.get(key);
45073
+ if (existing)
45074
+ return { promise: existing, joined: true };
45075
+ while (inFlightAdvisorCalls.size >= MAX_IN_FLIGHT_ADVISOR_CALLS) {
45076
+ const oldest = inFlightAdvisorCalls.keys().next().value;
45077
+ if (oldest === undefined)
45078
+ break;
45079
+ inFlightAdvisorCalls.delete(oldest);
45080
+ }
45081
+ inFlightByToolUseId.set(toolUseId, (inFlightByToolUseId.get(toolUseId) ?? 0) + 1);
45082
+ const release = () => {
45083
+ const left = (inFlightByToolUseId.get(toolUseId) ?? 1) - 1;
45084
+ if (left > 0)
45085
+ inFlightByToolUseId.set(toolUseId, left);
45086
+ else
45087
+ inFlightByToolUseId.delete(toolUseId);
45088
+ };
45089
+ let started;
45090
+ try {
45091
+ started = start();
45092
+ } catch (err) {
45093
+ release();
45094
+ throw err;
45095
+ }
45096
+ const promise = started.finally(() => {
45097
+ release();
45098
+ if (inFlightAdvisorCalls.get(key) === promise)
45099
+ inFlightAdvisorCalls.delete(key);
45100
+ });
45101
+ inFlightAdvisorCalls.set(key, promise);
45102
+ return { promise, joined: false };
45103
+ }
45104
+
45105
+ class SseFrameBuffer {
45106
+ buf = "";
45107
+ take(chunkText) {
45108
+ this.buf += chunkText;
45109
+ const events = [];
45110
+ while (true) {
45111
+ const lf = this.buf.indexOf(`
45112
+
45113
+ `);
45114
+ const crlf = this.buf.indexOf(`\r
45115
+ \r
45116
+ `);
45117
+ let idx = -1;
45118
+ let width = 2;
45119
+ if (crlf >= 0 && (lf < 0 || crlf < lf)) {
45120
+ idx = crlf;
45121
+ width = 4;
45122
+ } else if (lf >= 0) {
45123
+ idx = lf;
45124
+ }
45125
+ if (idx < 0)
45126
+ break;
45127
+ events.push(this.buf.slice(0, idx));
45128
+ this.buf = this.buf.slice(idx + width);
45129
+ }
45130
+ if (this.buf.length > MAX_SSE_BUFFER_CHARS) {
45131
+ this.buf = this.buf.slice(-MAX_SSE_BUFFER_CHARS);
45132
+ }
45133
+ return events;
45134
+ }
45135
+ reset() {
45136
+ this.buf = "";
45137
+ }
45138
+ }
45139
+ function createAdvisorStreamScanner(cfg, sessionId) {
45140
+ const frames = new SseFrameBuffer;
45141
+ return {
45142
+ push(chunkText) {
45143
+ extractAdvisorToolUseIds(chunkText, frames, sessionId);
45144
+ logAdvisorMarkers(cfg, chunkText);
45145
+ }
45146
+ };
45147
+ }
45148
+ function extractAdvisorToolUseIds(chunkText, frames, sessionId) {
45149
+ if (parseAdvisorIdsFromJsonText(chunkText, sessionId))
45150
+ return;
45151
+ let sawCompleteFrame = false;
45152
+ for (const event of frames.take(chunkText)) {
45153
+ sawCompleteFrame = true;
45154
+ if (!scanSseEventForAdvisorIds(event, sessionId))
45155
+ matchAdvisorIdsByRegex(event, sessionId);
45156
+ }
45157
+ if (!sawCompleteFrame)
45158
+ matchAdvisorIdsByRegex(chunkText, sessionId);
45159
+ }
45160
+ function scanSseEventForAdvisorIds(rawEvent, sessionId) {
45161
+ const dataLines = [];
45162
+ for (const line of rawEvent.split(/\r?\n/)) {
45163
+ if (!line.startsWith("data:"))
45164
+ continue;
45165
+ dataLines.push(line.slice(5).trimStart());
45063
45166
  }
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]);
45167
+ if (dataLines.length === 0)
45168
+ return false;
45169
+ const payload = dataLines.join(`
45170
+ `).trim();
45171
+ if (!payload || payload === "[DONE]")
45172
+ return true;
45173
+ return parseAdvisorIdsFromJsonText(payload, sessionId);
45174
+ }
45175
+ function parseAdvisorIdsFromJsonText(text, sessionId) {
45176
+ const trimmed = text.trimStart();
45177
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
45178
+ return false;
45179
+ let parsed;
45180
+ try {
45181
+ parsed = JSON.parse(trimmed);
45182
+ } catch {
45183
+ return false;
45067
45184
  }
45185
+ collectAdvisorIdsFromValue(parsed, 0, sessionId);
45186
+ return true;
45068
45187
  }
45069
- function rememberAdvisorToolUseId(id) {
45070
- if (advisorToolUseIds.has(id))
45188
+ function collectAdvisorIdsFromValue(value, depth, sessionId) {
45189
+ if (value === null || typeof value !== "object" || depth > MAX_WALK_DEPTH)
45190
+ return;
45191
+ if (Array.isArray(value)) {
45192
+ for (const item of value)
45193
+ collectAdvisorIdsFromValue(item, depth + 1, sessionId);
45071
45194
  return;
45072
- if (advisorToolUseIds.size >= MAX_TRACKED) {
45073
- const first = advisorToolUseIds.values().next().value;
45074
- if (first !== undefined)
45075
- advisorToolUseIds.delete(first);
45076
45195
  }
45077
- advisorToolUseIds.add(id);
45196
+ const obj = value;
45197
+ if (obj.type === "tool_use" && obj.name === ADVISOR_TOOL_NAME && typeof obj.id === "string" && obj.id.length > 0) {
45198
+ rememberAdvisorToolUseId(obj.id, sessionId);
45199
+ }
45200
+ for (const nested of Object.values(obj))
45201
+ collectAdvisorIdsFromValue(nested, depth + 1, sessionId);
45202
+ }
45203
+ function matchAdvisorIdsByRegex(text, sessionId) {
45204
+ for (const re of ADVISOR_ID_PATTERNS) {
45205
+ re.lastIndex = 0;
45206
+ let m;
45207
+ while ((m = re.exec(text)) !== null) {
45208
+ rememberAdvisorToolUseId(m[1], sessionId);
45209
+ }
45210
+ }
45078
45211
  }
45079
- function rewriteAdvisorToolResults(payload, getAdviceFor) {
45212
+ function rewriteAdvisorToolResults(payload, getAdviceFor, sessionId) {
45080
45213
  const messages = payload.messages;
45081
45214
  if (!Array.isArray(messages))
45082
45215
  return [];
@@ -45097,12 +45230,18 @@ function rewriteAdvisorToolResults(payload, getAdviceFor) {
45097
45230
  const toolUseId = block.tool_use_id;
45098
45231
  if (typeof toolUseId !== "string")
45099
45232
  continue;
45100
- if (!advisorToolUseIds.has(toolUseId))
45233
+ if (!lookupAdvisorCall(toolUseId, sessionId))
45101
45234
  continue;
45102
- const advice = getAdviceFor(toolUseId);
45103
- block.content = [{ type: "text", text: advice }];
45104
- if (block.is_error)
45235
+ const supplied = getAdviceFor(toolUseId);
45236
+ if (supplied === undefined)
45237
+ continue;
45238
+ const result = typeof supplied === "string" ? { text: supplied, isError: false } : supplied;
45239
+ block.content = [{ type: "text", text: result.text }];
45240
+ if (result.isError) {
45241
+ block.is_error = true;
45242
+ } else if (block.is_error) {
45105
45243
  block.is_error = false;
45244
+ }
45106
45245
  rewritten.push(toolUseId);
45107
45246
  }
45108
45247
  }
@@ -45111,7 +45250,34 @@ function rewriteAdvisorToolResults(payload, getAdviceFor) {
45111
45250
  function stubAdvisorAdvice(toolUseId) {
45112
45251
  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
45252
  }
45114
- function findPendingAdvisorToolResults(payload) {
45253
+ function prepareLegacyStubResult(cfg, toolUseId, sessionId) {
45254
+ const result = { text: stubAdvisorAdvice(toolUseId), isError: false };
45255
+ markAdvisorCallConsumed(toolUseId, result, sessionId);
45256
+ logAdvisorEvent(cfg, {
45257
+ kind: "advisor_rewrite",
45258
+ event: "advisor_rewrite",
45259
+ toolUseId,
45260
+ sessionId: sessionId ?? null,
45261
+ panel: [],
45262
+ originsByModel: {},
45263
+ failedModels: [],
45264
+ collector: null,
45265
+ collectorOrigin: null,
45266
+ resultOrigin: "stub",
45267
+ stubPath: ADVISOR_STUB_PATHS.LEGACY_STUB,
45268
+ isError: false
45269
+ });
45270
+ return result;
45271
+ }
45272
+ function missingAdvisorResult(toolUseId) {
45273
+ log(`[advisor] no prepared result for recorded call ${toolUseId} (stub path S2)`);
45274
+ warnAdvisor(`advisor call ${toolUseId}: claudish had no prepared result (internal error)`);
45275
+ return {
45276
+ text: `${ADVISOR_ERROR_PREFIX} claudish recorded advisor call ${toolUseId} but had no result prepared for it (internal error). ${ADVISOR_ERROR_SUFFIX}`,
45277
+ isError: true
45278
+ };
45279
+ }
45280
+ function findPendingAdvisorToolResults(payload, sessionId) {
45115
45281
  const messages = payload.messages;
45116
45282
  if (!Array.isArray(messages))
45117
45283
  return [];
@@ -45130,13 +45296,101 @@ function findPendingAdvisorToolResults(payload) {
45130
45296
  if (block.type !== "tool_result")
45131
45297
  continue;
45132
45298
  const toolUseId = block.tool_use_id;
45133
- if (typeof toolUseId === "string" && advisorToolUseIds.has(toolUseId)) {
45299
+ if (typeof toolUseId === "string" && lookupAdvisorCall(toolUseId, sessionId)) {
45134
45300
  found.push(toolUseId);
45135
45301
  }
45136
45302
  }
45137
45303
  }
45138
45304
  return found;
45139
45305
  }
45306
+ function toolResultText(content) {
45307
+ if (typeof content === "string")
45308
+ return content;
45309
+ if (!Array.isArray(content))
45310
+ return "";
45311
+ return content.map((b) => b && typeof b.text === "string" ? b.text : "").filter(Boolean).join(`
45312
+ `);
45313
+ }
45314
+ function advisorToolUseIdsInPayload(payload) {
45315
+ const ids = new Set;
45316
+ const messages = payload.messages;
45317
+ if (!Array.isArray(messages))
45318
+ return ids;
45319
+ for (const msg of messages) {
45320
+ if (!msg || typeof msg !== "object")
45321
+ continue;
45322
+ const content = msg.content;
45323
+ if (!Array.isArray(content))
45324
+ continue;
45325
+ for (const block of content) {
45326
+ if (!block || typeof block !== "object")
45327
+ continue;
45328
+ const b = block;
45329
+ if (b.type !== "tool_use" || b.name !== ADVISOR_TOOL_NAME)
45330
+ continue;
45331
+ if (typeof b.id === "string" && b.id.length > 0)
45332
+ ids.add(b.id);
45333
+ }
45334
+ }
45335
+ return ids;
45336
+ }
45337
+ function reportUnrecordedAdvisorCalls(cfg, payload, sessionId) {
45338
+ const messages = payload.messages;
45339
+ if (!Array.isArray(messages))
45340
+ return [];
45341
+ const advisorIds = advisorToolUseIdsInPayload(payload);
45342
+ if (advisorIds.size === 0)
45343
+ return [];
45344
+ const reported = [];
45345
+ for (const msg of messages) {
45346
+ if (!msg || typeof msg !== "object" || msg.role !== "user")
45347
+ continue;
45348
+ const content = msg.content;
45349
+ if (!Array.isArray(content))
45350
+ continue;
45351
+ for (const block of content) {
45352
+ if (!block || typeof block !== "object" || block.type !== "tool_result")
45353
+ continue;
45354
+ const toolUseId = block.tool_use_id;
45355
+ if (typeof toolUseId !== "string")
45356
+ continue;
45357
+ if (!advisorIds.has(toolUseId))
45358
+ continue;
45359
+ if (!NO_SUCH_ADVISOR_TOOL.test(toolResultText(block.content)))
45360
+ continue;
45361
+ if (lookupAdvisorCall(toolUseId, sessionId))
45362
+ continue;
45363
+ const memo = `${sessionKeyFor(sessionId)}\x00${toolUseId}`;
45364
+ if (reportedUnrecorded.has(memo))
45365
+ continue;
45366
+ if (reportedUnrecorded.size >= MAX_REPORTED_UNRECORDED) {
45367
+ const oldest = reportedUnrecorded.values().next().value;
45368
+ if (oldest !== undefined)
45369
+ reportedUnrecorded.delete(oldest);
45370
+ }
45371
+ reportedUnrecorded.add(memo);
45372
+ reported.push(toolUseId);
45373
+ const panel = cfg.models ?? [];
45374
+ logAdvisorEvent(cfg, {
45375
+ kind: "advisor_rewrite",
45376
+ event: "advisor_rewrite",
45377
+ toolUseId,
45378
+ sessionId: sessionId ?? null,
45379
+ panel,
45380
+ originsByModel: Object.fromEntries(panel.map((m) => [m, "absent"])),
45381
+ failedModels: [],
45382
+ collector: cfg.collector ?? null,
45383
+ collectorOrigin: cfg.collector ? "absent" : null,
45384
+ resultOrigin: "absent",
45385
+ stubPath: ADVISOR_STUB_PATHS.NOT_REWRITTEN,
45386
+ isError: true
45387
+ });
45388
+ log(`[advisor] call ${toolUseId} was never rewritten (stub path S10)`);
45389
+ warnAdvisor(`advisor call ${toolUseId} was not answered: claudish never saw the advisor tool call, so the model got "No such tool available: advisor"`);
45390
+ }
45391
+ }
45392
+ return reported;
45393
+ }
45140
45394
  function convertToOpenAIMessages(anthropicMessages) {
45141
45395
  return anthropicMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
45142
45396
  role: m.role,
@@ -45164,168 +45418,598 @@ function extractBlocksAsText(content) {
45164
45418
  }).filter(Boolean).join(`
45165
45419
  `);
45166
45420
  }
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
- };
45421
+ function describeAdvisorQuestion(input) {
45422
+ const clip = (s) => s.length > MAX_ADVISOR_QUESTION_CHARS ? `${s.slice(0, MAX_ADVISOR_QUESTION_CHARS)}\u2026` : s || null;
45423
+ if (typeof input === "string")
45424
+ return clip(input.trim());
45425
+ if (!input || typeof input !== "object" || Array.isArray(input))
45426
+ return null;
45427
+ const obj = input;
45428
+ for (const field of ADVISOR_QUESTION_FIELDS) {
45429
+ const value = obj[field];
45430
+ if (typeof value === "string" && value.trim().length > 0)
45431
+ return clip(value.trim());
45183
45432
  }
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]
45433
+ if (Object.keys(obj).length === 0)
45434
+ return null;
45435
+ try {
45436
+ return clip(JSON.stringify(obj));
45437
+ } catch {
45438
+ return null;
45439
+ }
45440
+ }
45441
+ function advisorQuestionMessage(question) {
45442
+ 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:
45443
+
45444
+ ${question}`;
45445
+ return "[claudish] The coding assistant paused the work above and consulted its advisor. " + `You are the advisor.
45446
+
45447
+ ` + `${body}
45448
+
45449
+ ` + "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.";
45450
+ }
45451
+ function prepareAdvisorPanelMessages(messages, toolUseId) {
45452
+ if (!Array.isArray(messages))
45453
+ return [];
45454
+ let question = null;
45455
+ const prepared = messages.map((msg) => {
45456
+ if (!msg || typeof msg !== "object" || !Array.isArray(msg.content))
45457
+ return msg;
45458
+ let changed = false;
45459
+ const content = msg.content.map((block) => {
45460
+ if (!block || typeof block !== "object")
45461
+ return block;
45462
+ if (block.type === "tool_use" && block.name === ADVISOR_TOOL_NAME && block.id === toolUseId) {
45463
+ question ??= describeAdvisorQuestion(block.input);
45464
+ return block;
45195
45465
  }
45196
- };
45466
+ if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
45467
+ changed = true;
45468
+ return {
45469
+ ...block,
45470
+ content: [{ type: "text", text: ADVISOR_PLUMBING_MARKER }],
45471
+ is_error: false
45472
+ };
45473
+ }
45474
+ return block;
45475
+ });
45476
+ return changed ? { ...msg, content } : msg;
45477
+ });
45478
+ prepared.push({
45479
+ role: "user",
45480
+ content: [{ type: "text", text: advisorQuestionMessage(question) }]
45481
+ });
45482
+ return prepared;
45483
+ }
45484
+ function routeOf(kind, wireModel) {
45485
+ const url = ADVISOR_ENDPOINTS[kind];
45486
+ return { kind, host: new URL(url).host, url, credential: kind, wireModel };
45487
+ }
45488
+ function advisorRouteFor(modelSpec, role) {
45489
+ const parsed = parseModelSpec(modelSpec);
45490
+ if (role === "collector" && isAnthropicModel(parsed)) {
45491
+ const model = parsed.model;
45492
+ const tier = anthropicTierAlias(model);
45493
+ if (!tier)
45494
+ return routeOf("anthropic", model);
45495
+ const resolved = findEntryByAlias(model)?.modelId ?? latestAnthropicTierModelId(tier);
45496
+ if (!resolved)
45497
+ return { ...routeOf("anthropic", model), unresolvedAlias: model };
45498
+ return routeOf("anthropic", resolved);
45197
45499
  }
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]
45500
+ const provider = parsed.provider;
45501
+ if (provider === "google" || provider === "gemini")
45502
+ return routeOf("google", parsed.model);
45503
+ if (provider === "openai" || provider === "oai")
45504
+ return routeOf("openai", parsed.model);
45505
+ if (!parsed.isExplicitProvider || provider === "openrouter") {
45506
+ return routeOf("openrouter", openRouterWireModelFor(null, parsed.model));
45507
+ }
45508
+ return routeOf("openrouter", openRouterWireModelFor(provider, parsed.model));
45509
+ }
45510
+ function openRouterIdOf(entry) {
45511
+ const fromAggregator = entry.aggregators?.find((a) => a.provider === "openrouter")?.externalId;
45512
+ if (fromAggregator)
45513
+ return fromAggregator;
45514
+ return entry.sources["openrouter-api"]?.externalId ?? null;
45515
+ }
45516
+ function lookupOpenRouterId(name) {
45517
+ const entries = getCatalogEntries();
45518
+ if (!entries)
45519
+ return { kind: "unknown" };
45520
+ const lower = name.toLowerCase();
45521
+ if (name.includes("/")) {
45522
+ const match = entries.find((e) => openRouterIdOf(e)?.toLowerCase() === lower);
45523
+ return { kind: "id", id: match ? openRouterIdOf(match) : name };
45524
+ }
45525
+ const byModelId = entries.find((e) => e.modelId.toLowerCase() === lower);
45526
+ if (byModelId) {
45527
+ const id = openRouterIdOf(byModelId);
45528
+ return id ? { kind: "id", id } : { kind: "not-served" };
45529
+ }
45530
+ const byAlias = entries.find((e) => e.aliases.some((a) => a.toLowerCase() === lower));
45531
+ if (byAlias) {
45532
+ const id = openRouterIdOf(byAlias);
45533
+ return id ? { kind: "id", id } : { kind: "not-served" };
45534
+ }
45535
+ const suffix = `/${lower}`;
45536
+ for (const entry of entries) {
45537
+ const id = openRouterIdOf(entry);
45538
+ if (id && (id.toLowerCase() === lower || id.toLowerCase().endsWith(suffix))) {
45539
+ return { kind: "id", id };
45540
+ }
45541
+ }
45542
+ return { kind: "unknown" };
45543
+ }
45544
+ function isOpenRouterVendorNamespace(vendor) {
45545
+ const entries = getCatalogEntries();
45546
+ if (!entries)
45547
+ return false;
45548
+ const prefix = `${vendor.toLowerCase()}/`;
45549
+ return entries.some((e) => openRouterIdOf(e)?.toLowerCase().startsWith(prefix));
45550
+ }
45551
+ function nativeVendorsForProvider(providerUid) {
45552
+ const vendors = new Set;
45553
+ try {
45554
+ for (const plan of readAllModelsCache()?.plans ?? []) {
45555
+ if (plan.routing?.providerUid !== providerUid)
45556
+ continue;
45557
+ for (const native of plan.routing.nativeModelProviders ?? [])
45558
+ vendors.add(native);
45212
45559
  }
45560
+ } catch {}
45561
+ return [...vendors];
45562
+ }
45563
+ function openRouterWireModelFor(provider, model) {
45564
+ const lookup = lookupOpenRouterId(model);
45565
+ if (provider === null || provider === "openrouter") {
45566
+ if (lookup.kind === "id")
45567
+ return lookup.id;
45568
+ if (lookup.kind === "unknown")
45569
+ return model;
45570
+ 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").');
45571
+ }
45572
+ const vendorPrefixed = `${provider}/${model}`;
45573
+ if (isOpenRouterVendorNamespace(provider))
45574
+ return vendorPrefixed;
45575
+ if (lookup.kind === "id")
45576
+ return lookup.id;
45577
+ for (const vendor of nativeVendorsForProvider(provider)) {
45578
+ if (isOpenRouterVendorNamespace(vendor))
45579
+ return `${vendor}/${model}`;
45580
+ }
45581
+ if (getCatalogEntries() === null)
45582
+ return vendorPrefixed;
45583
+ 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").');
45584
+ }
45585
+ function advisorCredentialsFor(models, collector) {
45586
+ const needed = new Set;
45587
+ const add = (spec, role) => {
45588
+ try {
45589
+ needed.add(advisorRouteFor(spec, role).credential);
45590
+ } catch {}
45213
45591
  };
45592
+ for (const m of models)
45593
+ add(m, "panel");
45594
+ if (collector && models.length > 1)
45595
+ add(collector, "collector");
45596
+ return needed;
45214
45597
  }
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);
45598
+ function isPlaceholderAnthropicKey(key) {
45599
+ return /placeholder/i.test(key);
45600
+ }
45601
+ async function resolveAdvisorCredential(credential) {
45602
+ const fromAuthority = async (provider, header) => {
45603
+ try {
45604
+ const auth = await credentials.getRequestAuth(provider, { model: "" });
45605
+ const bearer = auth.headers.Authorization?.replace(/^Bearer\s+/i, "").trim();
45606
+ const apiKey = auth.headers["x-api-key"]?.trim();
45607
+ return (header === "x-api-key" ? apiKey : bearer || apiKey) || undefined;
45608
+ } catch {
45609
+ return;
45610
+ }
45611
+ };
45612
+ if (credential === "anthropic") {
45613
+ const key = await fromAuthority(ADVISOR_AUTHORITY_PROVIDER.anthropic, "x-api-key");
45614
+ if (!key || key === process.env.ANTHROPIC_AUTH_TOKEN || isPlaceholderAnthropicKey(key)) {
45615
+ return;
45616
+ }
45617
+ return key;
45618
+ }
45619
+ if (credential === "google") {
45620
+ return await fromAuthority(ADVISOR_AUTHORITY_PROVIDER.google, "any") || process.env.GOOGLE_API_KEY?.trim() || undefined;
45621
+ }
45622
+ return fromAuthority(ADVISOR_AUTHORITY_PROVIDER[credential], "any");
45623
+ }
45624
+ function advisorTokenParamFor(route) {
45625
+ if (route.kind !== "openai")
45626
+ return "max_tokens";
45627
+ let fromCatalog;
45220
45628
  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)}`);
45629
+ fromCatalog = lookupModelTokenParam(route.wireModel);
45630
+ } catch {}
45631
+ if (fromCatalog === "max_tokens" || fromCatalog === "max_completion_tokens")
45632
+ return fromCatalog;
45633
+ return "max_completion_tokens";
45634
+ }
45635
+ function buildAdvisorRequest(route, messages, apiKeys, systemPrompt = ADVISOR_SYSTEM_PROMPT) {
45636
+ if (route.kind === "anthropic") {
45637
+ throw new Error(`${route.wireModel}: the Anthropic route is collector-only`);
45638
+ }
45639
+ const headers = {
45640
+ "Content-Type": "application/json",
45641
+ Authorization: `Bearer ${apiKeys[route.credential] ?? ""}`
45642
+ };
45643
+ if (route.kind === "openrouter") {
45644
+ headers["HTTP-Referer"] = "https://claudish.com";
45645
+ headers["X-Title"] = "Claudish Advisor";
45646
+ }
45647
+ const body = {
45648
+ model: route.wireModel,
45649
+ messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAIMessages(messages)]
45650
+ };
45651
+ body[advisorTokenParamFor(route)] = ADVISOR_MAX_OUTPUT_TOKENS;
45652
+ return { headers, body };
45653
+ }
45654
+ function stubOutcome(base, stubPath, reason, latencyMs, secrets, observed) {
45655
+ return {
45656
+ ...base,
45657
+ upstreamStatus: observed?.status ?? null,
45658
+ upstreamStatusSource: observed?.source ?? null,
45659
+ responseBytes: observed?.bytes ?? 0,
45660
+ latencyMs,
45661
+ origin: "stub",
45662
+ stubPath,
45663
+ reason: sanitizeAdvisorReason(reason, secrets)
45664
+ };
45665
+ }
45666
+ function summarizeErrorBody(bodyText) {
45667
+ let message = "";
45668
+ try {
45669
+ message = extractProviderMessage(JSON.parse(bodyText));
45670
+ } catch {
45671
+ message = bodyText;
45672
+ }
45673
+ const oneLine = String(message ?? "").replace(/\s+/g, " ").trim();
45674
+ return oneLine ? oneLine.slice(0, 200) : "(empty body)";
45675
+ }
45676
+ function describeFetchError(err, timeoutMs) {
45677
+ const e = err;
45678
+ if (e?.name === "AbortError" && timeoutMs) {
45679
+ return `timed out after ${Math.round(timeoutMs / 1000)}s`;
45680
+ }
45681
+ return `request failed: ${e?.message ?? String(err)}`;
45682
+ }
45683
+ async function executeAdvisorFetch(plan, fetchImpl) {
45684
+ const base = { role: plan.role, requestedModel: plan.requestedModel, route: plan.route };
45685
+ const secrets = credentialValuesInHeaders(plan.headers);
45686
+ const started = performance.now();
45687
+ const elapsed = () => Math.round(performance.now() - started);
45688
+ const controller = plan.timeoutMs ? new AbortController : undefined;
45689
+ const timer = controller ? setTimeout(() => controller.abort(), plan.timeoutMs) : undefined;
45690
+ const doFetch = fetchImpl ?? fetch;
45691
+ try {
45692
+ let resp;
45693
+ try {
45694
+ resp = await doFetch(plan.route.url, {
45695
+ method: "POST",
45696
+ headers: plan.headers,
45697
+ body: JSON.stringify(plan.body),
45698
+ signal: controller?.signal
45699
+ });
45700
+ } catch (err) {
45701
+ return stubOutcome(base, plan.errorStubPath, describeFetchError(err, plan.timeoutMs), elapsed(), secrets);
45230
45702
  }
45231
- const data = await resp.json();
45232
- return data.choices?.[0]?.message?.content ?? "(no response)";
45703
+ let bodyText;
45704
+ try {
45705
+ bodyText = await resp.text();
45706
+ } catch (err) {
45707
+ 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 });
45708
+ }
45709
+ const bodyStatus = extractUpstreamStatus(bodyText);
45710
+ const observed = {
45711
+ status: bodyStatus ?? resp.status,
45712
+ source: bodyStatus !== undefined ? "error.upstream_status" : "http_status",
45713
+ bytes: new TextEncoder().encode(bodyText).byteLength
45714
+ };
45715
+ if (!resp.ok || bodyStatus !== undefined && bodyStatus >= 400) {
45716
+ return stubOutcome(base, plan.errorStubPath, `HTTP ${observed.status}: ${summarizeErrorBody(bodyText)}`, elapsed(), secrets, observed);
45717
+ }
45718
+ let data;
45719
+ try {
45720
+ data = JSON.parse(bodyText);
45721
+ } catch {
45722
+ return stubOutcome(base, plan.emptyStubPath, `HTTP ${observed.status} but the response body is not JSON`, elapsed(), secrets, observed);
45723
+ }
45724
+ const text = plan.extractText(data);
45725
+ if (typeof text !== "string" || text.trim().length === 0) {
45726
+ return stubOutcome(base, plan.emptyStubPath, `HTTP ${observed.status} but the response carried no advice text`, elapsed(), secrets, observed);
45727
+ }
45728
+ return {
45729
+ ...base,
45730
+ upstreamStatus: observed.status,
45731
+ upstreamStatusSource: observed.source,
45732
+ responseBytes: observed.bytes,
45733
+ latencyMs: elapsed(),
45734
+ origin: "upstream",
45735
+ stubPath: null,
45736
+ text
45737
+ };
45233
45738
  } finally {
45234
- clearTimeout(timeout);
45739
+ if (timer)
45740
+ clearTimeout(timer);
45741
+ }
45742
+ }
45743
+ function extractChatCompletionText(data) {
45744
+ const content = data?.choices?.[0]?.message?.content;
45745
+ if (typeof content === "string")
45746
+ return content;
45747
+ if (Array.isArray(content)) {
45748
+ const joined = content.map((p) => typeof p?.text === "string" ? p.text : "").join("");
45749
+ return joined || undefined;
45750
+ }
45751
+ return;
45752
+ }
45753
+ function extractAnthropicText(data) {
45754
+ const blocks = data?.content;
45755
+ if (!Array.isArray(blocks))
45756
+ return;
45757
+ const text = blocks.find((b) => b?.type === "text")?.text;
45758
+ return typeof text === "string" ? text : undefined;
45759
+ }
45760
+ function errorMessageOf(err) {
45761
+ return err?.message ?? String(err);
45762
+ }
45763
+ async function callAdvisorModel(modelSpec, messages, apiKeys, fetchImpl) {
45764
+ let plan;
45765
+ try {
45766
+ const route = advisorRouteFor(modelSpec, "panel");
45767
+ const { headers, body } = buildAdvisorRequest(route, messages, apiKeys);
45768
+ plan = {
45769
+ role: "panel",
45770
+ requestedModel: modelSpec,
45771
+ route,
45772
+ headers,
45773
+ body,
45774
+ timeoutMs: ADVISOR_PANEL_TIMEOUT_MS,
45775
+ extractText: extractChatCompletionText,
45776
+ emptyStubPath: ADVISOR_STUB_PATHS.PANEL_EMPTY,
45777
+ errorStubPath: ADVISOR_STUB_PATHS.PANEL_ERROR
45778
+ };
45779
+ } catch (err) {
45780
+ const base = { role: "panel", requestedModel: modelSpec, route: null };
45781
+ return stubOutcome(base, ADVISOR_STUB_PATHS.PANEL_ERROR, `could not build the request: ${errorMessageOf(err)}`, 0, credentialValuesOf(apiKeys));
45235
45782
  }
45783
+ return executeAdvisorFetch(plan, fetchImpl);
45784
+ }
45785
+ function anthropicTierAlias(model) {
45786
+ const m = model.toLowerCase();
45787
+ return m === "opus" || m === "sonnet" || m === "haiku" ? m : null;
45236
45788
  }
45237
45789
  function isAnthropicModel(parsed) {
45238
45790
  const m = parsed.model.toLowerCase();
45239
45791
  return parsed.provider === "anthropic" || m.startsWith("claude-") || m === "haiku" || m === "sonnet" || m === "opus";
45240
45792
  }
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",
45793
+ function planAnthropicCollector(requestedModel, route, adviceText, apiKey) {
45794
+ return {
45795
+ role: "collector",
45796
+ requestedModel,
45797
+ route,
45246
45798
  headers: {
45247
45799
  "Content-Type": "application/json",
45248
45800
  "x-api-key": apiKey ?? "",
45249
45801
  "anthropic-version": "2023-06-01"
45250
45802
  },
45251
- body: JSON.stringify({
45252
- model: resolvedModel,
45803
+ body: {
45804
+ model: route.wireModel,
45253
45805
  max_tokens: 1024,
45254
45806
  system: COLLECTOR_SYSTEM_PROMPT,
45255
45807
  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)";
45808
+ },
45809
+ timeoutMs: ADVISOR_COLLECTOR_TIMEOUT_MS,
45810
+ extractText: extractAnthropicText,
45811
+ emptyStubPath: ADVISOR_STUB_PATHS.ANTHROPIC_COLLECTOR_EMPTY,
45812
+ errorStubPath: ADVISOR_STUB_PATHS.COLLECTOR_FAILED
45813
+ };
45262
45814
  }
45263
- async function callCollectorModel(collectorSpec, advice, apiKeys) {
45264
- const adviceText = advice.map((a, i) => `### Advisor ${i + 1} (${a.model})
45815
+ async function callCollectorModel(collectorSpec, advice, apiKeys, fetchImpl) {
45816
+ let plan;
45817
+ try {
45818
+ const adviceText = advice.map((a, i) => `### Advisor ${i + 1} (${a.model})
45265
45819
  ${a.text}`).join(`
45266
45820
 
45267
45821
  `);
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 });
45822
+ const route = advisorRouteFor(collectorSpec, "collector");
45823
+ if (route.unresolvedAlias) {
45824
+ 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`).");
45825
+ }
45826
+ if (route.kind === "anthropic") {
45827
+ plan = planAnthropicCollector(collectorSpec, route, adviceText, apiKeys.anthropic);
45305
45828
  } else {
45306
- sections.push(`## ${models[i]}
45307
- [Error: ${result.reason?.message ?? "unknown"}]`);
45829
+ const { headers, body } = buildAdvisorRequest(route, [], apiKeys, COLLECTOR_SYSTEM_PROMPT);
45830
+ body.messages = [
45831
+ { role: "system", content: COLLECTOR_SYSTEM_PROMPT },
45832
+ { role: "user", content: adviceText }
45833
+ ];
45834
+ plan = {
45835
+ role: "collector",
45836
+ requestedModel: collectorSpec,
45837
+ route,
45838
+ headers,
45839
+ body,
45840
+ timeoutMs: ADVISOR_COLLECTOR_TIMEOUT_MS,
45841
+ extractText: extractChatCompletionText,
45842
+ emptyStubPath: ADVISOR_STUB_PATHS.COLLECTOR_EMPTY,
45843
+ errorStubPath: ADVISOR_STUB_PATHS.COLLECTOR_FAILED
45844
+ };
45308
45845
  }
45846
+ } catch (err) {
45847
+ const base = {
45848
+ role: "collector",
45849
+ requestedModel: collectorSpec,
45850
+ route: null
45851
+ };
45852
+ return stubOutcome(base, ADVISOR_STUB_PATHS.COLLECTOR_FAILED, `could not build the request: ${errorMessageOf(err)}`, 0, credentialValuesOf(apiKeys));
45309
45853
  }
45310
- if (models.length === 1 && successfulAdvice.length === 1) {
45311
- return successfulAdvice[0].text;
45854
+ return executeAdvisorFetch(plan, fetchImpl);
45855
+ }
45856
+ function panelFailureSection(o) {
45857
+ return `## ${o.requestedModel}
45858
+ [Error: ${o.requestedModel} returned no advice \u2014 ${o.reason}]`;
45859
+ }
45860
+ function allPanelFailedText(panel) {
45861
+ if (panel.length === 0) {
45862
+ return `${ADVISOR_ERROR_PREFIX} No advisor model is configured, so no advice was produced. ${ADVISOR_ERROR_SUFFIX}`;
45312
45863
  }
45313
- if (!collector || successfulAdvice.length === 0) {
45314
- return sections.join(`
45864
+ const head = panel.length === 1 ? `The advisor model ${panel[0].requestedModel} did not return advice.` : `None of the ${panel.length} advisor models returned advice.`;
45865
+ const lines = panel.map((o) => `- ${o.requestedModel}: ${o.reason}`);
45866
+ return `${ADVISOR_ERROR_PREFIX} ${head}
45867
+ ${lines.join(`
45868
+ `)}
45869
+ ${ADVISOR_ERROR_SUFFIX}`;
45870
+ }
45871
+ function collectorFailedText(collector, sections) {
45872
+ return `${ADVISOR_ERROR_PREFIX} The collector model ${collector.requestedModel} failed to synthesize ` + `the advice: ${collector.reason}. The panel's unsynthesized answers follow.
45315
45873
 
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(`
45874
+ ` + sections.join(`
45324
45875
 
45325
45876
  `);
45877
+ }
45878
+ function modelCallRecord(kind, outcome, call) {
45879
+ return {
45880
+ kind,
45881
+ event: kind,
45882
+ toolUseId: call.toolUseId,
45883
+ sessionId: call.sessionId,
45884
+ role: outcome.role,
45885
+ requestedModel: outcome.requestedModel,
45886
+ provider: outcome.route?.kind ?? null,
45887
+ route: outcome.route ? { kind: outcome.route.kind, host: outcome.route.host } : null,
45888
+ routedModel: outcome.route?.wireModel ?? null,
45889
+ upstreamStatus: outcome.upstreamStatus,
45890
+ upstreamStatusSource: outcome.upstreamStatusSource,
45891
+ responseBytes: outcome.responseBytes,
45892
+ latencyMs: outcome.latencyMs,
45893
+ origin: outcome.origin,
45894
+ stubPath: outcome.stubPath,
45895
+ reason: outcome.reason ?? null
45896
+ };
45897
+ }
45898
+ function logAdvisorCallOutcome(cfg, o) {
45899
+ for (const p of o.panel)
45900
+ logAdvisorEvent(cfg, modelCallRecord("advisor_call", p, o));
45901
+ if (o.collectorOutcome) {
45902
+ logAdvisorEvent(cfg, modelCallRecord("advisor_collector_call", o.collectorOutcome, o));
45903
+ }
45904
+ const failedModels = [...o.panel, ...o.collectorOutcome ? [o.collectorOutcome] : []].filter((m) => m.origin !== "upstream").map((m) => m.requestedModel);
45905
+ logAdvisorEvent(cfg, {
45906
+ kind: "advisor_rewrite",
45907
+ event: "advisor_rewrite",
45908
+ toolUseId: o.toolUseId,
45909
+ sessionId: o.sessionId,
45910
+ panel: o.panel.map((p) => p.requestedModel),
45911
+ originsByModel: Object.fromEntries(o.panel.map((p) => [p.requestedModel, p.origin])),
45912
+ failedModels,
45913
+ collector: o.collector,
45914
+ collectorOrigin: o.collectorOutcome ? o.collectorOutcome.origin : o.collector ? "absent" : null,
45915
+ resultOrigin: o.resultOrigin,
45916
+ stubPath: o.stubPath,
45917
+ isError: o.result.isError
45918
+ });
45919
+ }
45920
+ function warnOnAdvisorFailures(o, warn = warnAdvisor) {
45921
+ const failed = [...o.panel, ...o.collectorOutcome ? [o.collectorOutcome] : []].filter((m) => m.origin !== "upstream");
45922
+ if (failed.length === 0 && o.resultOrigin === "upstream")
45923
+ return;
45924
+ const detail = failed.length > 0 ? failed.map((m) => `${m.role === "collector" ? "collector " : ""}${m.requestedModel}: ${m.reason}`).join("; ") : "no advisor model is configured";
45925
+ const verdict = o.resultOrigin === "upstream" ? "advice from the other models was still delivered" : "the model received an error report instead of advice";
45926
+ warn(`advisor call ${o.toolUseId} \u2014 ${detail} (${verdict})`);
45927
+ }
45928
+ function recoverUnassociatedAdvisorResult(cfg, outcome, warn = warnAdvisor) {
45929
+ const lost = [
45930
+ ...outcome.panel.map((p) => p.requestedModel),
45931
+ ...outcome.collectorOutcome ? [outcome.collectorOutcome.requestedModel] : []
45932
+ ];
45933
+ const result = {
45934
+ 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,
45935
+ isError: true
45936
+ };
45937
+ const sessionId = outcome.sessionId ?? undefined;
45938
+ rememberAdvisorToolUseId(outcome.toolUseId, sessionId);
45939
+ markAdvisorCallConsumed(outcome.toolUseId, result, sessionId);
45940
+ log(`[advisor] call ${outcome.toolUseId}: the result could not be associated with the tracked call (stub path ${ADVISOR_STUB_PATHS.PREPARED_RESULT_MISSING})`);
45941
+ if (cfg) {
45942
+ logAdvisorEvent(cfg, {
45943
+ kind: "advisor_rewrite",
45944
+ event: "advisor_rewrite",
45945
+ toolUseId: outcome.toolUseId,
45946
+ sessionId: outcome.sessionId,
45947
+ corrects: "advisor_rewrite",
45948
+ panel: outcome.panel.map((p) => p.requestedModel),
45949
+ originsByModel: Object.fromEntries(outcome.panel.map((p) => [p.requestedModel, "stub"])),
45950
+ failedModels: lost,
45951
+ collector: outcome.collector,
45952
+ collectorOrigin: outcome.collectorOutcome ? "stub" : null,
45953
+ resultOrigin: "stub",
45954
+ stubPath: ADVISOR_STUB_PATHS.PREPARED_RESULT_MISSING,
45955
+ isError: true
45956
+ });
45326
45957
  }
45958
+ 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`);
45959
+ return result;
45960
+ }
45961
+ async function runAdvisorCall(params) {
45962
+ const { toolUseId, messages, models, collector, apiKeys, fetchImpl } = params;
45963
+ const panelMessages = prepareAdvisorPanelMessages(messages, toolUseId);
45964
+ const panel = await Promise.all(models.map((m) => callAdvisorModel(m, panelMessages, apiKeys, fetchImpl)));
45965
+ const successful = panel.filter((o) => o.origin === "upstream");
45966
+ const sections = panel.map((o) => o.origin === "upstream" ? `## ${o.requestedModel}
45967
+ ${o.text}` : panelFailureSection(o));
45968
+ let result;
45969
+ let collectorOutcome = null;
45970
+ let resultOrigin;
45971
+ let stubPath = null;
45972
+ if (models.length === 1 && successful.length === 1) {
45973
+ result = { text: successful[0].text, isError: false };
45974
+ resultOrigin = "upstream";
45975
+ } else if (successful.length === 0) {
45976
+ result = { text: allPanelFailedText(panel), isError: true };
45977
+ resultOrigin = "stub";
45978
+ stubPath = ADVISOR_STUB_PATHS.ALL_PANEL_FAILED;
45979
+ } else if (!collector) {
45980
+ result = { text: sections.join(`
45981
+
45982
+ `), isError: false };
45983
+ resultOrigin = "upstream";
45984
+ } else {
45985
+ collectorOutcome = await callCollectorModel(collector, successful.map((o) => ({ model: o.requestedModel, text: o.text })), apiKeys, fetchImpl);
45986
+ if (collectorOutcome.origin === "upstream" && collectorOutcome.text !== undefined) {
45987
+ result = { text: collectorOutcome.text, isError: false };
45988
+ resultOrigin = "upstream";
45989
+ } else {
45990
+ log(`[advisor] collector ${collector} failed: ${collectorOutcome.reason}, falling back to concat`);
45991
+ result = { text: collectorFailedText(collectorOutcome, sections), isError: true };
45992
+ resultOrigin = "stub";
45993
+ stubPath = collectorOutcome.stubPath;
45994
+ }
45995
+ }
45996
+ const outcome = {
45997
+ toolUseId,
45998
+ sessionId: params.sessionId ?? null,
45999
+ panel,
46000
+ collector,
46001
+ collectorOutcome,
46002
+ resultOrigin,
46003
+ stubPath,
46004
+ result
46005
+ };
46006
+ log(`[advisor] call ${toolUseId}: resultOrigin=${resultOrigin}${stubPath ? ` stubPath=${stubPath}` : ""} ` + `origins=${panel.map((p) => `${p.requestedModel}:${p.origin}`).join(",")}` + (collectorOutcome ? ` collector=${collectorOutcome.requestedModel}:${collectorOutcome.origin}` : ""));
46007
+ if (params.cfg)
46008
+ logAdvisorCallOutcome(params.cfg, outcome);
46009
+ warnOnAdvisorFailures(outcome, params.warn);
46010
+ return outcome;
45327
46011
  }
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.
46012
+ 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
46013
 
45330
46014
  Review the conversation and provide concise, actionable advice. Focus on:
45331
46015
  - Architectural decisions and trade-offs
@@ -45337,13 +46021,523 @@ Be direct. Limit your response to 300-500 words.`, COLLECTOR_SYSTEM_PROMPT = `Yo
45337
46021
  - Identifies consensus points (where advisors agree)
45338
46022
  - Highlights disagreements and explains which perspective is stronger
45339
46023
  - Produces a clear, actionable recommendation
45340
- Be concise. Do not attribute advice to specific models.`;
46024
+ 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
46025
  var init_native_handler_advisor = __esm(() => {
46026
+ init_model_catalog();
46027
+ init_authority();
45342
46028
  init_logger();
46029
+ init_all_models_cache();
45343
46030
  init_catalog_client();
45344
46031
  init_catalog_query();
45345
46032
  init_model_parser();
45346
- advisorToolUseIds = new Set;
46033
+ init_anthropic_error();
46034
+ ORIGIN_RECORD_KINDS = new Set([
46035
+ "advisor_call",
46036
+ "advisor_collector_call",
46037
+ "advisor_rewrite"
46038
+ ]);
46039
+ ADVISOR_STUB_PATHS = Object.freeze({
46040
+ LEGACY_STUB: "S1",
46041
+ PREPARED_RESULT_MISSING: "S2",
46042
+ DISABLED_STUB: "S3",
46043
+ PANEL_EMPTY: "S4",
46044
+ ANTHROPIC_COLLECTOR_EMPTY: "S5",
46045
+ COLLECTOR_EMPTY: "S6",
46046
+ PANEL_ERROR: "S7",
46047
+ ALL_PANEL_FAILED: "S8",
46048
+ COLLECTOR_FAILED: "S9",
46049
+ NOT_REWRITTEN: "S10"
46050
+ });
46051
+ ADVISOR_PENDING_LIMITS = Object.freeze({
46052
+ maxCallsPerSession: 256,
46053
+ maxSessions: 64,
46054
+ ttlMs: 24 * 60 * 60 * 1000
46055
+ });
46056
+ pendingBySession = new Map;
46057
+ clock = Date.now;
46058
+ lastSweepAt = Number.NEGATIVE_INFINITY;
46059
+ MAX_SSE_BUFFER_CHARS = 256 * 1024;
46060
+ inFlightByToolUseId = new Map;
46061
+ inFlightAdvisorCalls = new Map;
46062
+ MAX_IN_FLIGHT_ADVISOR_CALLS = ADVISOR_PENDING_LIMITS.maxSessions * ADVISOR_PENDING_LIMITS.maxCallsPerSession;
46063
+ streamFrameBuffer = new SseFrameBuffer;
46064
+ ADVISOR_ID_PATTERNS = [
46065
+ /"type"\s*:\s*"tool_use"[^}]*?"id"\s*:\s*"([^"]+)"[^}]*?"name"\s*:\s*"advisor"/g,
46066
+ /"name"\s*:\s*"advisor"[^}]*?"id"\s*:\s*"([^"]+)"/g,
46067
+ /"id"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"advisor"/g
46068
+ ];
46069
+ NO_SUCH_ADVISOR_TOOL = /No such tool available:\s*advisor\b/;
46070
+ reportedUnrecorded = new Set;
46071
+ ADVISOR_QUESTION_FIELDS = [
46072
+ "question",
46073
+ "prompt",
46074
+ "query",
46075
+ "request",
46076
+ "task",
46077
+ "topic",
46078
+ "context",
46079
+ "input",
46080
+ "text"
46081
+ ];
46082
+ ADVISOR_ENDPOINTS = Object.freeze({
46083
+ google: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
46084
+ openai: "https://api.openai.com/v1/chat/completions",
46085
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
46086
+ anthropic: "https://api.anthropic.com/v1/messages"
46087
+ });
46088
+ ADVISOR_AUTHORITY_PROVIDER = Object.freeze({
46089
+ google: "google",
46090
+ openai: "openai",
46091
+ openrouter: "openrouter",
46092
+ anthropic: "native-anthropic"
46093
+ });
46094
+ });
46095
+
46096
+ // src/handlers/advisor-decorator.ts
46097
+ async function resolveAdvisorKeys(needed, inboundApiKey) {
46098
+ const anthropicKey = async () => {
46099
+ if (inboundApiKey && !isPlaceholderAnthropicKey(inboundApiKey))
46100
+ return inboundApiKey;
46101
+ return resolveAdvisorCredential("anthropic");
46102
+ };
46103
+ const [openrouter, google, openai, anthropic] = await Promise.all([
46104
+ needed.has("openrouter") ? resolveAdvisorCredential("openrouter") : undefined,
46105
+ needed.has("google") ? resolveAdvisorCredential("google") : undefined,
46106
+ needed.has("openai") ? resolveAdvisorCredential("openai") : undefined,
46107
+ needed.has("anthropic") ? anthropicKey() : undefined
46108
+ ]);
46109
+ return { openrouter, google, openai, anthropic };
46110
+ }
46111
+ function toolsOfferAdvisor(tools) {
46112
+ if (!Array.isArray(tools))
46113
+ return false;
46114
+ return tools.some((t) => {
46115
+ if (!t || typeof t !== "object")
46116
+ return false;
46117
+ const tool = t;
46118
+ return tool.type === ADVISOR_SERVER_TOOL_TYPE2 || tool.name === ADVISOR_TOOL_NAME2;
46119
+ });
46120
+ }
46121
+ function createAdvisorPresenceMonitor(warn = logStderr) {
46122
+ let consecutiveAbsent = 0;
46123
+ let warned = false;
46124
+ return {
46125
+ observe(payload) {
46126
+ if (warned)
46127
+ return;
46128
+ const tools = payload?.tools;
46129
+ if (!Array.isArray(tools) || tools.length === 0)
46130
+ return;
46131
+ if (toolsOfferAdvisor(tools)) {
46132
+ consecutiveAbsent = 0;
46133
+ return;
46134
+ }
46135
+ consecutiveAbsent++;
46136
+ log(`[advisor-swap] request offers ${tools.length} tool(s) but no advisor (${consecutiveAbsent} in a row)`);
46137
+ if (consecutiveAbsent < ADVISOR_ABSENT_WARN_AFTER)
46138
+ return;
46139
+ warned = true;
46140
+ 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.`);
46141
+ }
46142
+ };
46143
+ }
46144
+ function withAdvisorSwap(inner, cfg, deps = {}) {
46145
+ if (!cfg.enabled)
46146
+ return inner;
46147
+ if (inner instanceof AdvisorSwapHandler)
46148
+ return inner;
46149
+ return new AdvisorSwapHandler(inner, cfg, deps);
46150
+ }
46151
+
46152
+ class AdvisorSwapHandler {
46153
+ inner;
46154
+ cfg;
46155
+ deps;
46156
+ constructor(inner, cfg, deps = {}) {
46157
+ this.inner = inner;
46158
+ this.cfg = cfg;
46159
+ this.deps = deps;
46160
+ }
46161
+ async handle(c, payload) {
46162
+ if (c.get("advisorHandled") === true)
46163
+ return this.inner.handle(c, payload);
46164
+ c.set("advisorHandled", true);
46165
+ const sessionId = extractSessionId(payload);
46166
+ try {
46167
+ this.deps.presence?.observe(payload);
46168
+ } catch {}
46169
+ await applyAdvisorRequestSide(c, payload, this.cfg, sessionId, this.deps.resolveKeys ?? resolveAdvisorKeys);
46170
+ const response = await this.inner.handle(c, payload);
46171
+ return tapAdvisorResponse(response, this.cfg, sessionId);
46172
+ }
46173
+ async shutdown() {
46174
+ await this.inner.shutdown();
46175
+ }
46176
+ }
46177
+ async function applyAdvisorRequestSide(c, payload, cfg, sessionId, resolveKeys) {
46178
+ const target = payload.model;
46179
+ const sessionLabel = sessionId ?? NO_SESSION_BUCKET;
46180
+ const swapped = swapAdvisorToolInBody(payload);
46181
+ if (swapped) {
46182
+ c.set(ADVISOR_SWAPPED_CONTEXT_KEY, true);
46183
+ log(`[advisor-swap] replaced advisor_20260301 with function tool 'advisor' (model=${target}, session=${sessionLabel})`);
46184
+ logAdvisorEvent(cfg, {
46185
+ kind: "swap_applied",
46186
+ model: target,
46187
+ originalTool: swapped.originalTool,
46188
+ regularTool: swapped.regularTool
46189
+ });
46190
+ }
46191
+ const cachedResult = (id) => getAdvisorCall(id, sessionId)?.result;
46192
+ let rewrittenIds = [];
46193
+ if (cfg.models && cfg.models.length > 0) {
46194
+ const models = cfg.models;
46195
+ rewriteAdvisorToolResults(payload, cachedResult, sessionId);
46196
+ const pendingIds = findPendingAdvisorToolResults(payload, sessionId);
46197
+ if (pendingIds.length > 0) {
46198
+ const freshIds = [];
46199
+ const delivered = new Map;
46200
+ let apiKeys;
46201
+ for (const id of pendingIds) {
46202
+ if (cachedResult(id))
46203
+ continue;
46204
+ const runPanel = async () => {
46205
+ apiKeys ??= await resolveKeys(advisorCredentialsFor(models, cfg.collector), c.req.header("x-api-key"));
46206
+ const outcome = await runAdvisorCall({
46207
+ toolUseId: id,
46208
+ sessionId,
46209
+ messages: payload.messages,
46210
+ models,
46211
+ collector: cfg.collector ?? null,
46212
+ apiKeys,
46213
+ cfg
46214
+ });
46215
+ if (!markAdvisorCallConsumed(id, outcome.result, sessionId)) {
46216
+ return recoverUnassociatedAdvisorResult(cfg, outcome);
46217
+ }
46218
+ return outcome.result;
46219
+ };
46220
+ let { promise, joined } = joinOrStartAdvisorCall(id, sessionId, runPanel);
46221
+ if (joined) {
46222
+ log(`[advisor-swap] advisor call ${id} is already running for this session; joined it instead of running the panel again (session=${sessionLabel})`);
46223
+ try {
46224
+ delivered.set(id, await promise);
46225
+ continue;
46226
+ } catch (err) {
46227
+ log(`[advisor-swap] the in-flight advisor call ${id} failed (${errorMessage(err)}); running our own`);
46228
+ ({ promise, joined } = joinOrStartAdvisorCall(id, sessionId, runPanel));
46229
+ }
46230
+ }
46231
+ delivered.set(id, await promise);
46232
+ if (!joined)
46233
+ freshIds.push(id);
46234
+ }
46235
+ rewrittenIds = rewriteAdvisorToolResults(payload, (id) => cachedResult(id) ?? delivered.get(id) ?? missingAdvisorResult(id), sessionId);
46236
+ if (rewrittenIds.length > 0) {
46237
+ const replayed = rewrittenIds.filter((id) => !freshIds.includes(id));
46238
+ 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"}`);
46239
+ logAdvisorEvent(cfg, {
46240
+ kind: "multi_model_rewrite",
46241
+ ids: rewrittenIds,
46242
+ freshIds,
46243
+ models: cfg.models,
46244
+ collector: cfg.collector,
46245
+ model: target
46246
+ });
46247
+ }
46248
+ }
46249
+ } else {
46250
+ for (const id of findPendingAdvisorToolResults(payload, sessionId)) {
46251
+ if (!cachedResult(id))
46252
+ prepareLegacyStubResult(cfg, id, sessionId);
46253
+ }
46254
+ rewrittenIds = rewriteAdvisorToolResults(payload, (id) => cachedResult(id) ?? stubAdvisorAdvice(id), sessionId);
46255
+ if (rewrittenIds.length > 0) {
46256
+ log(`[advisor-swap] rewrote ${rewrittenIds.length} advisor tool_result(s) with stub advice (session=${sessionLabel}): ${rewrittenIds.join(", ")}`);
46257
+ logAdvisorEvent(cfg, {
46258
+ kind: "tool_result_rewritten",
46259
+ ids: rewrittenIds,
46260
+ model: target
46261
+ });
46262
+ }
46263
+ }
46264
+ reportUnrecordedAdvisorCalls(cfg, payload, sessionId);
46265
+ if (cfg.dumpBodies) {
46266
+ logAdvisorEvent(cfg, {
46267
+ kind: "request_body",
46268
+ swapApplied: !!swapped,
46269
+ rewrittenIds,
46270
+ model: target,
46271
+ body: trimForLog(payload)
46272
+ });
46273
+ }
46274
+ }
46275
+ function tapAdvisorResponse(response, cfg, sessionId) {
46276
+ const body = response.body;
46277
+ if (!body)
46278
+ return response;
46279
+ const contentType = response.headers.get("content-type") ?? "";
46280
+ const kind = contentType.includes("text/event-stream") ? "sse" : contentType.includes("json") ? "json" : null;
46281
+ if (!kind)
46282
+ return response;
46283
+ let toClient;
46284
+ let toScan;
46285
+ try {
46286
+ [toClient, toScan] = body.tee();
46287
+ } catch (err) {
46288
+ log(`[advisor-swap] response not scanned (${errorMessage(err)}); passed through untouched`);
46289
+ return response;
46290
+ }
46291
+ const clientReader = toClient.getReader();
46292
+ const scanReader = toScan.getReader();
46293
+ drainScanBranch(scanReader, kind, cfg, sessionId);
46294
+ const passthrough = new ReadableStream({
46295
+ async pull(controller) {
46296
+ try {
46297
+ const { done, value } = await clientReader.read();
46298
+ if (done)
46299
+ controller.close();
46300
+ else
46301
+ controller.enqueue(value);
46302
+ } catch (err) {
46303
+ controller.error(err);
46304
+ }
46305
+ },
46306
+ cancel(reason) {
46307
+ scanReader.cancel(reason).catch(() => {});
46308
+ return clientReader.cancel(reason);
46309
+ }
46310
+ }, { highWaterMark: 0 });
46311
+ return new Response(passthrough, {
46312
+ status: response.status,
46313
+ statusText: response.statusText,
46314
+ headers: response.headers
46315
+ });
46316
+ }
46317
+ async function drainScanBranch(reader, kind, cfg, sessionId) {
46318
+ const decoder = new TextDecoder;
46319
+ const scanner = kind === "sse" ? createAdvisorStreamScanner(cfg, sessionId) : null;
46320
+ let jsonText = "";
46321
+ let jsonTooLarge = false;
46322
+ const take = (text) => {
46323
+ if (!text)
46324
+ return;
46325
+ if (scanner) {
46326
+ scanner.push(text);
46327
+ } else if (!jsonTooLarge) {
46328
+ jsonText += text;
46329
+ if (jsonText.length > MAX_JSON_SCAN_CHARS) {
46330
+ jsonTooLarge = true;
46331
+ jsonText = "";
46332
+ }
46333
+ }
46334
+ };
46335
+ try {
46336
+ while (true) {
46337
+ const { done, value } = await reader.read();
46338
+ if (done)
46339
+ break;
46340
+ take(typeof value === "string" ? value : decoder.decode(value, { stream: true }));
46341
+ }
46342
+ take(decoder.decode());
46343
+ if (jsonTooLarge) {
46344
+ log(`[advisor-swap] non-stream response over ${MAX_JSON_SCAN_CHARS} chars was not scanned for advisor calls`);
46345
+ } else if (!scanner && jsonText.trim()) {
46346
+ recordAdvisorEventsFromResponseBody(cfg, JSON.parse(jsonText), sessionId);
46347
+ }
46348
+ } catch (err) {
46349
+ log(`[advisor-swap] response scan stopped: ${errorMessage(err)} (client stream unaffected)`);
46350
+ reader.cancel(err).catch(() => {});
46351
+ }
46352
+ }
46353
+ function errorMessage(err) {
46354
+ return err instanceof Error ? err.message : String(err);
46355
+ }
46356
+ function trimForLog(payload) {
46357
+ const TEXT_TRUNC = 400;
46358
+ const clone = structuredClone(payload);
46359
+ const trimStr = (s) => typeof s === "string" && s.length > TEXT_TRUNC ? `${s.slice(0, TEXT_TRUNC)}\u2026 [+${s.length - TEXT_TRUNC} chars]` : s;
46360
+ const walk = (v) => {
46361
+ if (typeof v === "string")
46362
+ return trimStr(v);
46363
+ if (Array.isArray(v))
46364
+ return v.map(walk);
46365
+ if (v && typeof v === "object") {
46366
+ const out = {};
46367
+ for (const [k, val] of Object.entries(v))
46368
+ out[k] = walk(val);
46369
+ return out;
46370
+ }
46371
+ return v;
46372
+ };
46373
+ return walk(clone);
46374
+ }
46375
+ 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;
46376
+ var init_advisor_decorator = __esm(() => {
46377
+ init_harness();
46378
+ init_logger();
46379
+ init_native_handler_advisor();
46380
+ MAX_JSON_SCAN_CHARS = 16 * 1024 * 1024;
46381
+ });
46382
+
46383
+ // src/handlers/fallback-handler.ts
46384
+ class FallbackHandler {
46385
+ candidates;
46386
+ lastSuccessIndex = 0;
46387
+ constructor(candidates) {
46388
+ this.candidates = candidates;
46389
+ }
46390
+ async handle(c, payload) {
46391
+ const errors = [];
46392
+ const startIndex = this.lastSuccessIndex;
46393
+ for (let attempt = 0;attempt < this.candidates.length; attempt++) {
46394
+ const idx = (startIndex + attempt) % this.candidates.length;
46395
+ const { name, handler } = this.candidates[idx];
46396
+ const isLast = attempt === this.candidates.length - 1;
46397
+ try {
46398
+ if (errors.length > 0 && handler instanceof ComposedHandler) {
46399
+ try {
46400
+ handler.setFallbackMeta(this.candidates.map((c) => c.name), errors.length);
46401
+ } catch {}
46402
+ }
46403
+ const response = await handler.handle(c, payload);
46404
+ if (response.ok) {
46405
+ this.lastSuccessIndex = idx;
46406
+ if (errors.length > 0) {
46407
+ logStderr(`[Fallback] ${name} succeeded after ${errors.length} failed attempt(s)`);
46408
+ if (handler instanceof ComposedHandler) {
46409
+ handler.getTokenTracker()?.setProviderDisplayName(name);
46410
+ }
46411
+ }
46412
+ return response;
46413
+ }
46414
+ const errorBody = await response.clone().text();
46415
+ if (!isRetryableError(response.status, errorBody, name)) {
46416
+ if (errors.length > 0) {
46417
+ errors.push({ provider: name, status: response.status, message: errorBody });
46418
+ return this.formatCombinedError(c, errors, payload.model);
46419
+ }
46420
+ return response;
46421
+ }
46422
+ errors.push({ provider: name, status: response.status, message: errorBody });
46423
+ if (!isLast) {
46424
+ if (hasQuotaExhaustionWording(errorBody)) {
46425
+ 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.`);
46426
+ } else {
46427
+ logStderr(`[Fallback] ${name} failed (HTTP ${response.status}), trying next provider...`);
46428
+ }
46429
+ }
46430
+ } catch (err) {
46431
+ errors.push({ provider: name, status: 0, message: err.message });
46432
+ if (!isLast) {
46433
+ logStderr(`[Fallback] ${name} error: ${err.message}, trying next provider...`);
46434
+ }
46435
+ }
46436
+ }
46437
+ return this.formatCombinedError(c, errors, payload.model);
46438
+ }
46439
+ formatCombinedError(c, errors, modelName) {
46440
+ const summary = errors.map((e) => ` ${e.provider}: HTTP ${e.status || "ERR"} \u2014 ${truncate(parseErrorMessage(e.message), 150)}`).join(`
46441
+ `);
46442
+ logStderr(`[Fallback] All ${errors.length} provider(s) failed for ${modelName || "model"}:
46443
+ ${summary}`);
46444
+ return c.json({
46445
+ error: {
46446
+ type: "all_providers_failed",
46447
+ message: `All ${errors.length} providers failed for model '${modelName || "unknown"}'`,
46448
+ attempts: errors.map((e) => ({
46449
+ provider: e.provider,
46450
+ status: e.status,
46451
+ error: truncate(parseErrorMessage(e.message), 200)
46452
+ }))
46453
+ }
46454
+ }, exhaustedChainStatus(errors));
46455
+ }
46456
+ async shutdown() {
46457
+ for (const { handler } of this.candidates) {
46458
+ if (typeof handler.shutdown === "function") {
46459
+ await handler.shutdown();
46460
+ }
46461
+ }
46462
+ }
46463
+ }
46464
+ function isRetryableError(status, errorBody, provider) {
46465
+ if (hasQuotaExhaustionWording(errorBody))
46466
+ return true;
46467
+ const upstream = status === 400 ? extractUpstreamStatus(errorBody) : undefined;
46468
+ if (upstream === 401 || upstream === 403 || upstream === 402 || upstream === 429) {
46469
+ return true;
46470
+ }
46471
+ if (status === 401 || status === 403)
46472
+ return true;
46473
+ if (status === 402)
46474
+ return true;
46475
+ if (status === 404)
46476
+ return true;
46477
+ if (status === 429)
46478
+ return true;
46479
+ const lower = errorBody.toLowerCase();
46480
+ if (status === 422) {
46481
+ if (lower.includes("not available") || lower.includes("model not found") || lower.includes("not supported")) {
46482
+ return true;
46483
+ }
46484
+ }
46485
+ if (status === 400) {
46486
+ 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")) {
46487
+ return true;
46488
+ }
46489
+ if (provider?.toLowerCase().includes("antigravity") && lower.includes("invalid argument")) {
46490
+ return true;
46491
+ }
46492
+ if (isProvider(provider, "opencodezen") && (lower.includes("upstream request failed") || lower.includes("error from provider ("))) {
46493
+ return true;
46494
+ }
46495
+ }
46496
+ if (status === 500) {
46497
+ if (lower.includes("insufficient balance") || lower.includes("insufficient credit") || lower.includes("quota exceeded") || lower.includes("billing")) {
46498
+ return true;
46499
+ }
46500
+ }
46501
+ return false;
46502
+ }
46503
+ function exhaustedChainStatus(errors) {
46504
+ if (errors.length === 0)
46505
+ return 400;
46506
+ const isTransient = (e) => {
46507
+ if (e.status === 429 || e.status === 503)
46508
+ return true;
46509
+ if (hasQuotaExhaustionWording(e.message))
46510
+ return true;
46511
+ const upstream = e.status === 400 ? extractUpstreamStatus(e.message) : undefined;
46512
+ return upstream === 429 || upstream === 503;
46513
+ };
46514
+ return errors.every(isTransient) ? 503 : 400;
46515
+ }
46516
+ function isProvider(provider, needle) {
46517
+ if (!provider)
46518
+ return false;
46519
+ return provider.toLowerCase().replace(/[^a-z0-9]/g, "").includes(needle);
46520
+ }
46521
+ function parseErrorMessage(body) {
46522
+ try {
46523
+ const parsed = JSON.parse(body);
46524
+ if (typeof parsed.error === "string")
46525
+ return parsed.error;
46526
+ if (typeof parsed.error?.message === "string")
46527
+ return parsed.error.message;
46528
+ if (typeof parsed.message === "string")
46529
+ return parsed.message;
46530
+ } catch {}
46531
+ return body;
46532
+ }
46533
+ function truncate(s, max) {
46534
+ return s.length > max ? `${s.slice(0, max)}...` : s;
46535
+ }
46536
+ var init_fallback_handler = __esm(() => {
46537
+ init_logger();
46538
+ init_composed_handler();
46539
+ init_anthropic_error();
46540
+ init_quota_exhaustion();
45347
46541
  });
45348
46542
 
45349
46543
  // src/handlers/shared/thinking-signature.ts
@@ -45375,36 +46569,6 @@ function stripUnsignedThinkingBlocks(messages) {
45375
46569
  }
45376
46570
 
45377
46571
  // 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
46572
  class NativeHandler {
45409
46573
  apiKey;
45410
46574
  baseUrl;
@@ -45423,65 +46587,7 @@ class NativeHandler {
45423
46587
  if (strippedThinking > 0) {
45424
46588
  log(`[Native] stripped ${strippedThinking} unsigned thinking block(s) from history for ${target} (foreign-provider origin)`);
45425
46589
  }
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
- }
46590
+ const advisorSwapped = c.get(ADVISOR_SWAPPED_CONTEXT_KEY) === true;
45485
46591
  log(`
45486
46592
  === [NATIVE] Claude Code \u2192 Anthropic API Request ===`);
45487
46593
  log(`[Native] x-api-key: ${originalHeaders["x-api-key"] ? maskCredential(originalHeaders["x-api-key"]) : "(not set)"}`);
@@ -45515,7 +46621,7 @@ class NativeHandler {
45515
46621
  const { stripped, changed } = stripAdvisorBeta(incomingBeta);
45516
46622
  if (changed) {
45517
46623
  log(`[Native][advisor-swap] stripped advisor-tool beta; before=${incomingBeta} after=${stripped ?? "(empty)"}`);
45518
- logAdvisorEvent(advisorCfg, {
46624
+ logAdvisorEvent(loadAdvisorSwapConfig(this.advisorModels, this.advisorCollector), {
45519
46625
  kind: "beta_stripped",
45520
46626
  before: incomingBeta,
45521
46627
  after: stripped ?? ""
@@ -45552,7 +46658,6 @@ class NativeHandler {
45552
46658
  controller.enqueue(value);
45553
46659
  const chunkText = decoder.decode(value, { stream: true });
45554
46660
  buffer += chunkText;
45555
- recordAdvisorEventsFromChunk(advisorCfg, chunkText);
45556
46661
  const lines = buffer.split(`
45557
46662
  `);
45558
46663
  buffer = lines.pop() || "";
@@ -45582,11 +46687,6 @@ class NativeHandler {
45582
46687
  log(`
45583
46688
  === [NATIVE] Response ===`);
45584
46689
  log(JSON.stringify(data, null, 2));
45585
- if (advisorCfg.enabled) {
45586
- try {
45587
- recordAdvisorEventsFromChunk(advisorCfg, JSON.stringify(data));
45588
- } catch {}
45589
- }
45590
46690
  const responseHeaders = { "Content-Type": "application/json" };
45591
46691
  if (anthropicResponse.headers.has("anthropic-version")) {
45592
46692
  responseHeaders["anthropic-version"] = anthropicResponse.headers.get("anthropic-version");
@@ -45599,30 +46699,10 @@ class NativeHandler {
45599
46699
  }
45600
46700
  async shutdown() {}
45601
46701
  }
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
46702
  var init_native_handler = __esm(() => {
45622
46703
  init_authority();
45623
- init_op_source();
45624
46704
  init_logger();
45625
- init_profile_config();
46705
+ init_advisor_decorator();
45626
46706
  init_native_handler_advisor();
45627
46707
  init_anthropic_error();
45628
46708
  });
@@ -46463,6 +47543,10 @@ async function createProxyServer(port, _openrouterApiKey, model, monitorMode = f
46463
47543
  log(`[Proxy] behavior hooks load skipped: ${err instanceof Error ? err.message : String(err)}`);
46464
47544
  }
46465
47545
  const nativeHandler = new NativeHandler(anthropicApiKey, options.advisorModels, options.advisorCollector);
47546
+ const advisorPresence = createAdvisorPresenceMonitor();
47547
+ const withAdvisor = (handler, presence) => withAdvisorSwap(handler, loadAdvisorSwapConfig(options.advisorModels, options.advisorCollector), {
47548
+ presence
47549
+ });
46466
47550
  const requestShapingOpts = {
46467
47551
  effortOverride: isEffortLevel(options.effortOverride) ? options.effortOverride : undefined,
46468
47552
  modelParams: options.modelParams,
@@ -46872,9 +47956,9 @@ ${plan.hint}` : `[Route] ${plan.reason}`;
46872
47956
  if (!monitorMode && options.classifier?.enabled && isAutoModeClassifierRequest(body)) {
46873
47957
  log(`[Classifier] auto-mode permission classifier \u2192 native Anthropic (model ${body.model} \u2192 ${options.classifier.model})`);
46874
47958
  rewriteClassifierForNative(body, options.classifier.model);
46875
- return nativeHandler.handle(c, body);
47959
+ return await withAdvisor(nativeHandler).handle(c, body);
46876
47960
  }
46877
- const handler = await getHandlerForRequest(body.model);
47961
+ const handler = withAdvisor(await getHandlerForRequest(body.model), advisorPresence);
46878
47962
  return await handler.handle(c, body);
46879
47963
  } catch (e) {
46880
47964
  log(`[Proxy] Error: ${e}`);
@@ -46930,8 +48014,10 @@ var init_proxy_server = __esm(() => {
46930
48014
  init_authority();
46931
48015
  init_hooks();
46932
48016
  init_behavior();
48017
+ init_advisor_decorator();
46933
48018
  init_composed_handler();
46934
48019
  init_fallback_handler();
48020
+ init_native_handler_advisor();
46935
48021
  init_native_handler();
46936
48022
  init_anthropic_error();
46937
48023
  init_logger();
@@ -56287,7 +57373,11 @@ function parseAdvisorFlag(value) {
56287
57373
  } else {
56288
57374
  collector = collectorPart;
56289
57375
  }
56290
- return { models, collector };
57376
+ return {
57377
+ models,
57378
+ collector,
57379
+ collectorDefaulted: models.length > 1 && collectorPart === undefined
57380
+ };
56291
57381
  }
56292
57382
  async function parseArgs(args) {
56293
57383
  const config = {
@@ -56435,7 +57525,8 @@ async function parseArgs(args) {
56435
57525
  const parsed = parseAdvisorFlag(modelsArg);
56436
57526
  config.advisorModels = parsed.models;
56437
57527
  config.advisorCollector = parsed.collector;
56438
- config.monitor = true;
57528
+ config.advisorCollectorDefaulted = parsed.collectorDefaulted;
57529
+ config.advisor = true;
56439
57530
  } else if (arg === "--stdin") {
56440
57531
  config.stdin = true;
56441
57532
  } else if (arg === "--free") {
@@ -56669,10 +57760,10 @@ Usage: claudish --models --provider <slug>`);
56669
57760
  if (config._sawVerbose && !config.interactive && !config.claudeArgs.includes("--verbose") && !config.claudeArgs.includes("-v")) {
56670
57761
  config.claudeArgs.push("--verbose");
56671
57762
  }
57763
+ if ((config.monitor || config.advisor) && process.env.ANTHROPIC_API_KEY?.includes("placeholder")) {
57764
+ delete process.env.ANTHROPIC_API_KEY;
57765
+ }
56672
57766
  if (config.monitor) {
56673
- if (process.env.ANTHROPIC_API_KEY?.includes("placeholder")) {
56674
- delete process.env.ANTHROPIC_API_KEY;
56675
- }
56676
57767
  if (!config.quiet) {
56677
57768
  console.log("[claudish] Monitor mode enabled - proxying to real Anthropic API");
56678
57769
  console.log("[claudish] Using Claude Code's native authentication");
@@ -57639,7 +58730,7 @@ ${h("OPTIONS")}
57639
58730
  ${green("--stdin")} Read prompt from stdin (large prompts / piping)
57640
58731
  ${green("--free")} Show only FREE models in the interactive selector
57641
58732
  ${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)
58733
+ ${green("--advisor")} ${yellow('"m1,m2[:collector]"')} Multi-model advisor replacement (works with any --model)
57643
58734
  ${green("--model-params")} ${yellow('"k=v,..."')} Extra request params merged into the payload (e.g. reasoning.mode=pro)
57644
58735
  ${green("--effort-override")} ${yellow("<level>")} Pin reasoning effort verbatim, skipping the per-model clamp
57645
58736
  ${green("--pro-on-ultracode")} Apply the model's catalog preset while in ultracode (opt-in)
@@ -65601,12 +66692,34 @@ function shouldHideIncidentalAnthropicKey(config, env = process.env) {
65601
66692
  return false;
65602
66693
  return !wantsAnthropicApiBilling(config, env);
65603
66694
  }
66695
+ function scrubInheritedClaudishPlaceholders(env) {
66696
+ const removed = [];
66697
+ for (const name of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]) {
66698
+ if (isClaudishPlaceholderCredential(name, env[name])) {
66699
+ delete env[name];
66700
+ removed.push(name);
66701
+ }
66702
+ }
66703
+ return { removed };
66704
+ }
66705
+ function isClaudishPlaceholderCredential(name, value) {
66706
+ if (name === "ANTHROPIC_API_KEY")
66707
+ return value === CLAUDISH_PLACEHOLDER_API_KEY;
66708
+ if (name === "ANTHROPIC_AUTH_TOKEN")
66709
+ return value === CLAUDISH_PLACEHOLDER_AUTH_TOKEN;
66710
+ return false;
66711
+ }
66712
+ function isRealAnthropicEnvCredential(env, name) {
66713
+ const value = env[name];
66714
+ return Boolean(value) && !isClaudishPlaceholderCredential(name, value);
66715
+ }
65604
66716
  function hasResolvableAnthropicAuth(deps = {}) {
65605
66717
  const env = deps.env ?? process.env;
65606
66718
  const fileExists = deps.fileExists ?? existsSync31;
65607
66719
  const keychainProbe = deps.keychainProbe ?? defaultKeychainAnthropicProbe;
65608
- if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN)
66720
+ if (isRealAnthropicEnvCredential(env, "ANTHROPIC_API_KEY") || isRealAnthropicEnvCredential(env, "ANTHROPIC_AUTH_TOKEN")) {
65609
66721
  return true;
66722
+ }
65610
66723
  if (fileExists(join41(homedir36(), ".claude", ".credentials.json")))
65611
66724
  return true;
65612
66725
  return keychainProbe();
@@ -65614,8 +66727,11 @@ function hasResolvableAnthropicAuth(deps = {}) {
65614
66727
  function shouldPreserveNativeAuth(config) {
65615
66728
  return hasNativeAnthropicMapping(config) || classifierPassthroughEnabled(config) && hasResolvableAnthropicAuth();
65616
66729
  }
66730
+ function isAdvisorNativeSession(config) {
66731
+ return Boolean(config.advisor) && !config.model && !config.modelChain;
66732
+ }
65617
66733
  function isProxyAuthMode(config) {
65618
- return !config.monitor && !shouldPreserveNativeAuth(config);
66734
+ return !config.monitor && !isAdvisorNativeSession(config) && !shouldPreserveNativeAuth(config);
65619
66735
  }
65620
66736
  function managedSettingsPath() {
65621
66737
  if (isWindows2()) {
@@ -66012,9 +67128,48 @@ function resolveContextWindowEnv(realWindow, processEnv = process.env) {
66012
67128
  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
67129
  };
66014
67130
  }
67131
+ function resolveAdvisorToolEnv(config, processEnv = process.env) {
67132
+ if (!config.advisor)
67133
+ return { vars: {}, source: "off" };
67134
+ if (processEnv[ADVISOR_TOOL_ENV_VAR] !== undefined)
67135
+ return { vars: {}, source: "inherited" };
67136
+ return { vars: { [ADVISOR_TOOL_ENV_VAR]: "1" }, source: "claudish" };
67137
+ }
67138
+ function discoverUserAdvisorModel(claudeArgs = [], cwd = process.cwd()) {
67139
+ const sources = userSettingsFileCandidates(cwd).filter((file) => existsSync31(file));
67140
+ const idx = claudeArgs.indexOf("--settings");
67141
+ const settingsArg = idx === -1 ? undefined : claudeArgs[idx + 1];
67142
+ if (settingsArg)
67143
+ sources.push(settingsArg);
67144
+ const managed = managedSettingsPath();
67145
+ if (existsSync31(managed))
67146
+ sources.push(managed);
67147
+ let effective;
67148
+ for (const source of sources) {
67149
+ const layer = parseSettingsArgSafe(source);
67150
+ if (!layer || !("advisorModel" in layer))
67151
+ continue;
67152
+ const value = layer.advisorModel;
67153
+ effective = typeof value === "string" && value !== "" ? value : undefined;
67154
+ }
67155
+ return effective;
67156
+ }
67157
+ function resolveAdvisorModelArg(config, cwd = process.cwd()) {
67158
+ if (!config.advisor)
67159
+ return { args: [], source: "off" };
67160
+ const userChoice = discoverUserAdvisorModel(config.claudeArgs, cwd);
67161
+ if (userChoice)
67162
+ return { args: [], model: userChoice, source: "inherited" };
67163
+ return {
67164
+ args: ["--advisor", CLAUDISH_CHILD_ADVISOR_MODEL],
67165
+ model: CLAUDISH_CHILD_ADVISOR_MODEL,
67166
+ source: "claudish"
67167
+ };
67168
+ }
66015
67169
  async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66016
67170
  const hasProfileMappings = config.modelOpus || config.modelSonnet || config.modelHaiku || config.modelSubagent;
66017
- const modelId = config.model || (hasProfileMappings || config.monitor ? undefined : "unknown");
67171
+ const advisorNativeSession = isAdvisorNativeSession(config);
67172
+ const modelId = config.model || (hasProfileMappings || config.monitor || advisorNativeSession ? undefined : "unknown");
66018
67173
  const portMatch = proxyUrl.match(/:(\d+)/);
66019
67174
  const port = portMatch ? portMatch[1] : "unknown";
66020
67175
  const proxyAuthMode = isProxyAuthMode(config);
@@ -66026,6 +67181,7 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66026
67181
  return 1;
66027
67182
  }
66028
67183
  const userStatusLineCommand = discoverUserStatusLineCommand(config.claudeArgs);
67184
+ const advisorModelArg = resolveAdvisorModelArg(config);
66029
67185
  const {
66030
67186
  path: tempSettingsPath,
66031
67187
  statusLine,
@@ -66034,6 +67190,7 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66034
67190
  mergeUserSettingsIfPresent(config, tempSettingsPath, statusLine, proxyAuthMode);
66035
67191
  const claudeArgs = [];
66036
67192
  claudeArgs.push("--settings", tempSettingsPath);
67193
+ claudeArgs.push(...advisorModelArg.args);
66037
67194
  if (config.interactive) {
66038
67195
  if (config.autoApprove) {
66039
67196
  claudeArgs.push("--dangerously-skip-permissions");
@@ -66059,12 +67216,14 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66059
67216
  }
66060
67217
  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
67218
  const modelDisplayName = modelId || config.profile || "default";
67219
+ const advisorToolEnv = resolveAdvisorToolEnv(config);
66062
67220
  const env = {
66063
67221
  ...process.env,
66064
67222
  ANTHROPIC_BASE_URL: proxyUrl,
66065
67223
  [ENV.CLAUDISH_ACTIVE_MODEL_NAME]: modelDisplayName,
66066
67224
  CLAUDISH_IS_LOCAL: isLocalModel ? "true" : "false",
66067
- [ENV.CLAUDISH_TOKEN_FILE]: tokenFilePath
67225
+ [ENV.CLAUDISH_TOKEN_FILE]: tokenFilePath,
67226
+ ...advisorToolEnv.vars
66068
67227
  };
66069
67228
  if (modelId) {
66070
67229
  const parsedSpec = parseModelSpec(modelId);
@@ -66073,9 +67232,15 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66073
67232
  env[ENV.CLAUDISH_PROVIDER_NAME] = providerDisplayName;
66074
67233
  }
66075
67234
  }
67235
+ if (advisorToolEnv.source !== "off") {
67236
+ log(`[claude-runner] ${ADVISOR_TOOL_ENV_VAR}=${env[ADVISOR_TOOL_ENV_VAR]} (${advisorToolEnv.source})`);
67237
+ }
67238
+ if (advisorModelArg.source !== "off") {
67239
+ log(`[claude-runner] child advisor model=${advisorModelArg.model} (${advisorModelArg.source}` + `${advisorModelArg.source === "inherited" ? "; user setting kept, no --advisor passed" : " via --advisor"})`);
67240
+ }
66076
67241
  let hidAnthropicApiKey = false;
66077
67242
  delete env.CLAUDECODE;
66078
- if (config.monitor) {
67243
+ if (config.monitor || advisorNativeSession) {
66079
67244
  delete env.ANTHROPIC_API_KEY;
66080
67245
  delete env.ANTHROPIC_AUTH_TOKEN;
66081
67246
  if (modelId) {
@@ -66088,7 +67253,11 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66088
67253
  env[ENV.ANTHROPIC_SMALL_FAST_MODEL] = modelId;
66089
67254
  }
66090
67255
  if (shouldPreserveNativeAuth(config)) {
66091
- if (shouldHideIncidentalAnthropicKey(config)) {
67256
+ const scrubbed = scrubInheritedClaudishPlaceholders(env);
67257
+ if (scrubbed.removed.length > 0) {
67258
+ log(`[claude-runner] Removed inherited claudish placeholder credentials: ${scrubbed.removed.join(", ")}`);
67259
+ }
67260
+ if (shouldHideIncidentalAnthropicKey(config, env)) {
66092
67261
  delete env.ANTHROPIC_API_KEY;
66093
67262
  hidAnthropicApiKey = true;
66094
67263
  }
@@ -66096,8 +67265,8 @@ async function runClaudeWithProxy(config, proxyUrl, onCleanup) {
66096
67265
  if (classifierPassthroughEnabled(config)) {
66097
67266
  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
67267
  }
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";
67268
+ env.ANTHROPIC_API_KEY = CLAUDISH_PLACEHOLDER_API_KEY;
67269
+ env.ANTHROPIC_AUTH_TOKEN = CLAUDISH_PLACEHOLDER_AUTH_TOKEN;
66101
67270
  const realWindow = await computeMainThreadContextWindow(config);
66102
67271
  const contextEnv = resolveContextWindowEnv(realWindow, process.env);
66103
67272
  Object.assign(env, contextEnv.vars);
@@ -66311,7 +67480,7 @@ var restoreTerminal = null, macosKeychainAnthropicResult, defaultKeychainAnthrop
66311
67480
  macosKeychainAnthropicResult = false;
66312
67481
  }
66313
67482
  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;
67483
+ }, 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
67484
  var init_claude_runner = __esm(() => {
66316
67485
  init_model_catalog();
66317
67486
  init_config2();
@@ -66596,6 +67765,226 @@ var init_catalog_warm = __esm(() => {
66596
67765
  ];
66597
67766
  });
66598
67767
 
67768
+ // src/advisor-startup.ts
67769
+ function isClaudeCodeBoolTrue(value) {
67770
+ if (value === undefined)
67771
+ return false;
67772
+ const v = value.trim().toLowerCase();
67773
+ return v === "1" || v === "true" || v === "yes" || v === "on";
67774
+ }
67775
+ function routeAdvisorModel(modelSpec, role) {
67776
+ return advisorRouteFor(modelSpec, role);
67777
+ }
67778
+ function advisorCredentialEnvName(credential) {
67779
+ if (credential === "anthropic")
67780
+ return "ANTHROPIC_API_KEY";
67781
+ return getProviderByName(ADVISOR_AUTHORITY_PROVIDER[credential])?.apiKeyEnvVar || `a ${credential} API key`;
67782
+ }
67783
+ function alsoAcceptedEnvName(credential) {
67784
+ return credential === "google" ? "GOOGLE_API_KEY" : null;
67785
+ }
67786
+ async function resolveAdvisorCredentials(needed) {
67787
+ const present = async (credential) => Boolean(await resolveAdvisorCredential(credential));
67788
+ const list = [...needed];
67789
+ const results = await Promise.all(list.map(present));
67790
+ const out = {};
67791
+ list.forEach((c, i) => {
67792
+ out[c] = results[i];
67793
+ });
67794
+ return out;
67795
+ }
67796
+ function advisorModelStatus(model, route, presence) {
67797
+ const credentialName = advisorCredentialEnvName(route.credential);
67798
+ if (route.unresolvedAlias) {
67799
+ return {
67800
+ model,
67801
+ route,
67802
+ callable: false,
67803
+ credentialName,
67804
+ 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`
67805
+ };
67806
+ }
67807
+ return {
67808
+ model,
67809
+ route,
67810
+ callable: presence[route.credential] === true,
67811
+ credentialName
67812
+ };
67813
+ }
67814
+ function describeMissingCredential2(s) {
67815
+ const also = alsoAcceptedEnvName(s.route.credential);
67816
+ return `${s.model} calls ${s.route.host} and needs ${s.credentialName}${also ? ` (or ${also})` : ""}; ` + "none found in env, config, keychain or 1Password";
67817
+ }
67818
+ function describeUncallable(s) {
67819
+ return s.unresolved ?? describeMissingCredential2(s);
67820
+ }
67821
+ function formatCarriesTools(format) {
67822
+ try {
67823
+ const out = format.convertTools({
67824
+ tools: [
67825
+ {
67826
+ name: "advisor",
67827
+ description: "startup probe",
67828
+ input_schema: { type: "object", properties: {}, additionalProperties: false }
67829
+ }
67830
+ ]
67831
+ }, false);
67832
+ return Array.isArray(out) && out.length > 0;
67833
+ } catch {
67834
+ return true;
67835
+ }
67836
+ }
67837
+ function pinnedFormatForTransport(transport, modelName) {
67838
+ switch (transport) {
67839
+ case "ollamacloud":
67840
+ return new OllamaAPIFormat(modelName);
67841
+ default:
67842
+ return null;
67843
+ }
67844
+ }
67845
+ function resolveMainModelFact(model) {
67846
+ const native = nativeRouteFor(model);
67847
+ if (native)
67848
+ return { model, providerName: native.displayName, carriesTools: true };
67849
+ const resolution = resolveModelProvider(model);
67850
+ const definitionName = resolution.catalogName === "gemini" ? "google" : resolution.catalogName;
67851
+ const definition = definitionName ? getProviderByName(definitionName) : undefined;
67852
+ const format = definition ? pinnedFormatForTransport(definition.transport, resolution.modelName) : null;
67853
+ const providerName = resolution.category === "unknown" ? "unresolved (routed on the first request)" : definition?.displayName || resolution.providerName;
67854
+ return {
67855
+ model,
67856
+ providerName,
67857
+ carriesTools: format ? formatCarriesTools(format) : true,
67858
+ wireFormat: format?.getName()
67859
+ };
67860
+ }
67861
+ function mainModelSpecs(config) {
67862
+ if (config.modelChain && config.modelChain.length > 0)
67863
+ return [...config.modelChain];
67864
+ if (config.model)
67865
+ return [config.model];
67866
+ const tiers = [config.modelOpus, config.modelSonnet, config.modelHaiku, config.modelSubagent];
67867
+ return [...new Set(tiers.filter((m) => typeof m === "string" && m.length > 0))];
67868
+ }
67869
+ function modelIdentity(spec) {
67870
+ const model = parseModelSpec(spec).model.toLowerCase();
67871
+ const slash = model.lastIndexOf("/");
67872
+ return slash >= 0 ? model.slice(slash + 1) : model;
67873
+ }
67874
+ function panelMembersMatchingMain(panel, mainModels) {
67875
+ const main = new Set(mainModels.map(modelIdentity));
67876
+ return panel.filter((p) => main.has(modelIdentity(p)));
67877
+ }
67878
+ function refusalBeforeCredentials(facts) {
67879
+ const disable = facts.childEnv[ADVISOR_DISABLE_ENV_VAR];
67880
+ if (isClaudeCodeBoolTrue(disable)) {
67881
+ 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.`;
67882
+ }
67883
+ const toolless = facts.mainModels.filter((m) => !m.carriesTools);
67884
+ if (toolless.length > 0) {
67885
+ const named = toolless.map((m) => `${m.model} (${m.providerName}${m.wireFormat ? `, ${m.wireFormat}` : ""})`).join(", ");
67886
+ 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.";
67887
+ }
67888
+ if (facts.panel.length === 0) {
67889
+ return 'no advisor panel models were given. Use --advisor "model1[,model2][:collector]".';
67890
+ }
67891
+ return null;
67892
+ }
67893
+ function describeRoute(s) {
67894
+ return `${s.model} -> ${s.route.host} (${s.credentialName}, billed per token)`;
67895
+ }
67896
+ function decideAdvisorStartup(facts) {
67897
+ const early = refusalBeforeCredentials(facts);
67898
+ if (early)
67899
+ return { kind: "refuse", reason: early };
67900
+ const missingPanel = facts.panelStatus.filter((s) => !s.callable);
67901
+ if (missingPanel.length > 0) {
67902
+ return {
67903
+ kind: "refuse",
67904
+ reason: `advisor panel model${missingPanel.length > 1 ? "s" : ""} cannot be called: ` + `${missingPanel.map(describeUncallable).join("; ")}. ` + "Set the key, or remove the model from --advisor."
67905
+ };
67906
+ }
67907
+ let effectiveCollector = facts.collector;
67908
+ let collectorLine = "none";
67909
+ const cs = facts.collectorStatus;
67910
+ if (cs && !cs.callable) {
67911
+ if (!facts.collectorDefaulted) {
67912
+ return {
67913
+ kind: "refuse",
67914
+ reason: `collector ${describeUncallable(cs)}. ` + 'Set the key, name another collector ("a,b:collector"), or end the value with ":" for no collector.'
67915
+ };
67916
+ }
67917
+ effectiveCollector = null;
67918
+ 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";
67919
+ } else if (cs) {
67920
+ collectorLine = describeRoute(cs);
67921
+ }
67922
+ const notice = ["[claudish] --advisor is on for this launch"];
67923
+ notice.push(" panel (panel calls never use a subscription):");
67924
+ for (const s of facts.panelStatus)
67925
+ notice.push(` ${describeRoute(s)}`);
67926
+ notice.push(` collector: ${collectorLine}`);
67927
+ const main = facts.mainModels.length === 0 ? "your Claude Code session" : facts.mainModels.map((m) => `${m.model} via ${m.providerName}`).join(" -> ");
67928
+ notice.push(` main model: ${main}`);
67929
+ const envValue = facts.childEnv[ADVISOR_TOOL_ENV_VAR];
67930
+ if (facts.toolEnv.source === "inherited") {
67931
+ 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"));
67932
+ } else {
67933
+ notice.push(` ${ADVISOR_TOOL_ENV_VAR}=1 set by claudish`);
67934
+ }
67935
+ const includesMain = panelMembersMatchingMain(facts.panel, facts.mainModels.map((m) => m.model));
67936
+ if (includesMain.length > 0) {
67937
+ notice.push(` note: the panel includes the main model (${includesMain.join(", ")})`);
67938
+ }
67939
+ notice.push(" cost: each advisor call sends the FULL conversation to every panel model, uncached; " + "cost grows with panel size and transcript length");
67940
+ return { kind: "proceed", notice, effectiveCollector };
67941
+ }
67942
+ async function evaluateAdvisorStartup(config, toolEnv, childEnv = process.env, deps = { resolveCredentials: resolveAdvisorCredentials }) {
67943
+ if (!config.advisor)
67944
+ return null;
67945
+ const panel = config.advisorModels ?? [];
67946
+ const collector = config.advisorCollector ?? null;
67947
+ const mainModels = mainModelSpecs(config).map(resolveMainModelFact);
67948
+ const early = refusalBeforeCredentials({ childEnv, mainModels, panel });
67949
+ if (early)
67950
+ return { kind: "refuse", reason: early };
67951
+ let panelRoutes;
67952
+ let collectorRoute;
67953
+ try {
67954
+ panelRoutes = panel.map((m) => routeAdvisorModel(m, "panel"));
67955
+ collectorRoute = collector ? routeAdvisorModel(collector, "collector") : null;
67956
+ } catch (err) {
67957
+ return {
67958
+ kind: "refuse",
67959
+ reason: err instanceof Error ? err.message : String(err)
67960
+ };
67961
+ }
67962
+ const needed = new Set(panelRoutes.map((r) => r.credential));
67963
+ if (collectorRoute)
67964
+ needed.add(collectorRoute.credential);
67965
+ const presence = await deps.resolveCredentials(needed);
67966
+ return decideAdvisorStartup({
67967
+ panel,
67968
+ collector,
67969
+ collectorDefaulted: config.advisorCollectorDefaulted === true,
67970
+ mainModels,
67971
+ childEnv,
67972
+ toolEnv,
67973
+ panelStatus: panel.map((m, i) => advisorModelStatus(m, panelRoutes[i], presence)),
67974
+ collectorStatus: collector && collectorRoute ? advisorModelStatus(collector, collectorRoute, presence) : null
67975
+ });
67976
+ }
67977
+ var ADVISOR_DISABLE_ENV_VAR = "CLAUDE_CODE_DISABLE_ADVISOR_TOOL";
67978
+ var init_advisor_startup = __esm(() => {
67979
+ init_ollama_api_format();
67980
+ init_claude_runner();
67981
+ init_native_handler_advisor();
67982
+ init_model_parser();
67983
+ init_native_route();
67984
+ init_provider_definitions();
67985
+ init_provider_resolver();
67986
+ });
67987
+
66599
67988
  // src/tui/viz/text.ts
66600
67989
  function columns(n, fn, arg = "width") {
66601
67990
  if (!Number.isFinite(n))
@@ -69482,11 +70871,12 @@ Team Status`);
69482
70871
  process.exit(1);
69483
70872
  }
69484
70873
  const hasProfileTiers = cliConfig.modelOpus || cliConfig.modelSonnet || cliConfig.modelHaiku || cliConfig.modelSubagent;
69485
- if (cliConfig.interactive && !cliConfig.monitor && !cliConfig.model && !hasProfileTiers) {
70874
+ const advisorNativeSession = isAdvisorNativeSession(cliConfig);
70875
+ if (cliConfig.interactive && !cliConfig.monitor && !advisorNativeSession && !cliConfig.model && !hasProfileTiers) {
69486
70876
  cliConfig.model = await traceSpan("startup:model-select", () => selectModel({ freeOnly: cliConfig.freeOnly }).catch(handlePromptExit), { mayIncludeUserPrompt: true });
69487
70877
  console.log("");
69488
70878
  }
69489
- if (!cliConfig.interactive && !cliConfig.monitor && !cliConfig.model && !hasProfileTiers) {
70879
+ if (!cliConfig.interactive && !cliConfig.monitor && !advisorNativeSession && !cliConfig.model && !hasProfileTiers) {
69490
70880
  console.error("Error: Model must be specified in non-interactive mode");
69491
70881
  console.error("Use --model <model> flag, set CLAUDISH_MODEL env var, or use --profile");
69492
70882
  console.error("Try: claudish --models");
@@ -69546,16 +70936,36 @@ Team Status`);
69546
70936
  }
69547
70937
  }
69548
70938
  }
70939
+ const warmOutcome = await traceSpan("startup:catalog-warm", () => warmCatalogIfNeeded(cliConfig));
70940
+ if (warmOutcome === "hard_fail") {
70941
+ process.exit(1);
70942
+ }
70943
+ if (cliConfig.advisor) {
70944
+ await Promise.resolve().then(() => init_advisor_startup());
70945
+ const decision = await traceSpan("startup:advisor-check", () => evaluateAdvisorStartup(cliConfig, resolveAdvisorToolEnv(cliConfig)));
70946
+ if (decision?.kind === "refuse") {
70947
+ process.stderr.write(`[claudish] Error: --advisor cannot work in this launch: ${decision.reason}
70948
+ `);
70949
+ process.exit(1);
70950
+ }
70951
+ if (decision?.kind === "proceed") {
70952
+ cliConfig.advisorCollector = decision.effectiveCollector;
70953
+ await Promise.resolve().then(() => init_catalog_client());
70954
+ if (getCatalogEntries() === null) {
70955
+ const panel = cliConfig.advisorModels ?? [];
70956
+ 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.`);
70957
+ }
70958
+ process.stderr.write(`${decision.notice.join(`
70959
+ `)}
70960
+ `);
70961
+ }
70962
+ }
69549
70963
  if (cliConfig.stdin) {
69550
70964
  const stdinInput = await traceSpan("startup:stdin-read", () => readStdin());
69551
70965
  if (stdinInput.trim()) {
69552
70966
  cliConfig.claudeArgs = [stdinInput, ...cliConfig.claudeArgs];
69553
70967
  }
69554
70968
  }
69555
- const warmOutcome = await traceSpan("startup:catalog-warm", () => warmCatalogIfNeeded(cliConfig));
69556
- if (warmOutcome === "hard_fail") {
69557
- process.exit(1);
69558
- }
69559
70969
  const port = cliConfig.port || await traceSpan("startup:find-port", () => findAvailablePort(DEFAULT_PORT_RANGE.start, DEFAULT_PORT_RANGE.end));
69560
70970
  const explicitModel = typeof cliConfig.model === "string" ? cliConfig.model : undefined;
69561
70971
  const modelMap = {