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.
- package/dist/index.js +1870 -460
- 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.
|
|
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
|
|
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
|
|
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
|
-
|
|
44927
|
+
logAdvisorMarkers(cfg, JSON.stringify(body));
|
|
45032
44928
|
} catch {}
|
|
45033
44929
|
}
|
|
45034
|
-
function
|
|
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 =
|
|
44944
|
+
i = text.indexOf(needle, i);
|
|
45050
44945
|
if (i < 0)
|
|
45051
44946
|
break;
|
|
45052
|
-
const ctx =
|
|
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
|
|
45059
|
-
|
|
45060
|
-
|
|
45061
|
-
|
|
45062
|
-
|
|
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
|
-
|
|
45065
|
-
|
|
45066
|
-
|
|
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
|
|
45070
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
45233
|
+
if (!lookupAdvisorCall(toolUseId, sessionId))
|
|
45101
45234
|
continue;
|
|
45102
|
-
const
|
|
45103
|
-
|
|
45104
|
-
|
|
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
|
|
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" &&
|
|
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
|
|
45168
|
-
const
|
|
45169
|
-
|
|
45170
|
-
|
|
45171
|
-
|
|
45172
|
-
|
|
45173
|
-
|
|
45174
|
-
|
|
45175
|
-
|
|
45176
|
-
|
|
45177
|
-
|
|
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 (
|
|
45185
|
-
return
|
|
45186
|
-
|
|
45187
|
-
|
|
45188
|
-
|
|
45189
|
-
|
|
45190
|
-
|
|
45191
|
-
|
|
45192
|
-
|
|
45193
|
-
|
|
45194
|
-
|
|
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
|
|
45199
|
-
|
|
45200
|
-
|
|
45201
|
-
|
|
45202
|
-
|
|
45203
|
-
|
|
45204
|
-
|
|
45205
|
-
|
|
45206
|
-
|
|
45207
|
-
|
|
45208
|
-
|
|
45209
|
-
|
|
45210
|
-
|
|
45211
|
-
|
|
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
|
-
|
|
45216
|
-
|
|
45217
|
-
|
|
45218
|
-
|
|
45219
|
-
const
|
|
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
|
-
|
|
45222
|
-
|
|
45223
|
-
|
|
45224
|
-
|
|
45225
|
-
|
|
45226
|
-
|
|
45227
|
-
|
|
45228
|
-
|
|
45229
|
-
|
|
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
|
-
|
|
45232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45242
|
-
|
|
45243
|
-
|
|
45244
|
-
|
|
45245
|
-
|
|
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:
|
|
45252
|
-
model:
|
|
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
|
-
|
|
45259
|
-
|
|
45260
|
-
|
|
45261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45269
|
-
|
|
45270
|
-
|
|
45271
|
-
|
|
45272
|
-
|
|
45273
|
-
|
|
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
|
-
|
|
45307
|
-
|
|
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
|
-
|
|
45311
|
-
|
|
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
|
-
|
|
45314
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
66100
|
-
env.ANTHROPIC_AUTH_TOKEN =
|
|
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
|
-
|
|
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 = {
|