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