@khalilgharbaoui/opencode-claude-code-plugin 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -28
- package/dist/index.d.ts +54 -1
- package/dist/index.js +1209 -867
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +116 -6
package/dist/index.js
CHANGED
|
@@ -819,7 +819,7 @@ function parseSystemInit(msg) {
|
|
|
819
819
|
}
|
|
820
820
|
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
821
821
|
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
822
|
-
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription
|
|
822
|
+
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription.`;
|
|
823
823
|
return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
|
|
824
824
|
}
|
|
825
825
|
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
@@ -2873,6 +2873,13 @@ function clearCompression(sessionKey2) {
|
|
|
2873
2873
|
|
|
2874
2874
|
// src/types.ts
|
|
2875
2875
|
var READ_ONLY_PERMISSION_MODE = "read-only";
|
|
2876
|
+
var DEFAULT_PROXY_TOOL_NAMES = [
|
|
2877
|
+
"Bash",
|
|
2878
|
+
"Edit",
|
|
2879
|
+
"Write",
|
|
2880
|
+
"WebFetch",
|
|
2881
|
+
"Task"
|
|
2882
|
+
];
|
|
2876
2883
|
|
|
2877
2884
|
// src/permission-presets.ts
|
|
2878
2885
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
@@ -2960,6 +2967,23 @@ function resolvePermissionPreset(settings, defaultProxyTools) {
|
|
|
2960
2967
|
function isUnknownPreset(resolved) {
|
|
2961
2968
|
return resolved !== null && "unknown" in resolved;
|
|
2962
2969
|
}
|
|
2970
|
+
var NO_PERMISSION_PRESET = "none";
|
|
2971
|
+
function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAULT_PROXY_TOOL_NAMES) {
|
|
2972
|
+
const settings = options !== null && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
2973
|
+
const resolved = resolvePermissionPreset(settings, defaultProxyTools);
|
|
2974
|
+
if (resolved === null) {
|
|
2975
|
+
return { provider, preset: NO_PERMISSION_PRESET, applied: false, overrides: [] };
|
|
2976
|
+
}
|
|
2977
|
+
if (isUnknownPreset(resolved)) {
|
|
2978
|
+
return { provider, preset: resolved.unknown, applied: false, overrides: [] };
|
|
2979
|
+
}
|
|
2980
|
+
return {
|
|
2981
|
+
provider,
|
|
2982
|
+
preset: resolved.preset,
|
|
2983
|
+
applied: true,
|
|
2984
|
+
overrides: resolved.overridden
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2963
2987
|
function isReadOnlyPermissionMode(mode) {
|
|
2964
2988
|
return mode === READ_ONLY_PERMISSION_MODE;
|
|
2965
2989
|
}
|
|
@@ -4654,6 +4678,9 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
4654
4678
|
accounts,
|
|
4655
4679
|
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
4656
4680
|
mcpServers,
|
|
4681
|
+
permissionPresets: Object.entries(providers).map(
|
|
4682
|
+
([name, entry]) => summarizePermissionPreset(name, entry?.options)
|
|
4683
|
+
),
|
|
4657
4684
|
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
4658
4685
|
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
4659
4686
|
anthropicApiKeyInEnv: Boolean(
|
|
@@ -4747,6 +4774,13 @@ function describeAuth(auth) {
|
|
|
4747
4774
|
return "not checked";
|
|
4748
4775
|
}
|
|
4749
4776
|
}
|
|
4777
|
+
function describePermissionPresets(rows) {
|
|
4778
|
+
if (rows.length === 0) return NO_PERMISSION_PRESET;
|
|
4779
|
+
return rows.map((row) => {
|
|
4780
|
+
const suffix = row.preset === NO_PERMISSION_PRESET || row.applied ? "" : " (unknown, nothing applied)";
|
|
4781
|
+
return `${row.provider}: ${row.preset}${suffix}`;
|
|
4782
|
+
}).join(", ");
|
|
4783
|
+
}
|
|
4750
4784
|
function formatDoctorReport(report) {
|
|
4751
4785
|
const lines = [];
|
|
4752
4786
|
lines.push(DOCTOR_MARKER);
|
|
@@ -4761,10 +4795,22 @@ function formatDoctorReport(report) {
|
|
|
4761
4795
|
lines.push(`| accounts | ${list(report.accounts)} |`);
|
|
4762
4796
|
lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
|
|
4763
4797
|
lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
|
|
4798
|
+
lines.push(`| permissionPreset | ${describePermissionPresets(report.permissionPresets)} |`);
|
|
4764
4799
|
lines.push(`| transport | ${report.transport} |`);
|
|
4765
4800
|
lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
|
|
4766
4801
|
lines.push(`| turnStats | ${report.turnStats} |`);
|
|
4767
4802
|
lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
|
|
4803
|
+
const overriding = report.permissionPresets.filter((row) => row.overrides.length > 0);
|
|
4804
|
+
if (overriding.length > 0) {
|
|
4805
|
+
lines.push("");
|
|
4806
|
+
lines.push("**Permission preset overrides**");
|
|
4807
|
+
for (const row of overriding) {
|
|
4808
|
+
lines.push("");
|
|
4809
|
+
lines.push(`\`${row.provider}\` (${row.preset}) replaced:`);
|
|
4810
|
+
lines.push("");
|
|
4811
|
+
for (const override of row.overrides) lines.push(`- ${override}`);
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4768
4814
|
lines.push("");
|
|
4769
4815
|
lines.push("**Live `claude` processes**");
|
|
4770
4816
|
lines.push("");
|
|
@@ -4874,6 +4920,7 @@ async function gatherDoctorReport(options) {
|
|
|
4874
4920
|
accounts: base.accounts,
|
|
4875
4921
|
proxyTools: base.proxyTools,
|
|
4876
4922
|
mcpServers: base.mcpServers,
|
|
4923
|
+
permissionPresets: base.permissionPresets,
|
|
4877
4924
|
transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
|
|
4878
4925
|
planModeQuestion: base.planModeQuestion,
|
|
4879
4926
|
turnStats: options.turnStats,
|
|
@@ -4904,916 +4951,1041 @@ Could not build the report: ${message}`;
|
|
|
4904
4951
|
}
|
|
4905
4952
|
}
|
|
4906
4953
|
|
|
4907
|
-
// src/
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
return
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4954
|
+
// src/agent-models.ts
|
|
4955
|
+
import { readFile, readdir } from "fs/promises";
|
|
4956
|
+
import path6 from "path";
|
|
4957
|
+
|
|
4958
|
+
// src/models.ts
|
|
4959
|
+
var PROVIDER_ID = "claude-code";
|
|
4960
|
+
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
4961
|
+
var reasoningVariants = {
|
|
4962
|
+
low: { reasoningEffort: "low" },
|
|
4963
|
+
medium: { reasoningEffort: "medium" },
|
|
4964
|
+
high: { reasoningEffort: "high" },
|
|
4965
|
+
xhigh: { reasoningEffort: "xhigh" },
|
|
4966
|
+
max: { reasoningEffort: "max" }
|
|
4967
|
+
};
|
|
4968
|
+
var baseCapabilities = {
|
|
4969
|
+
temperature: false,
|
|
4970
|
+
attachment: true,
|
|
4971
|
+
toolcall: true,
|
|
4972
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
4973
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
4974
|
+
interleaved: false
|
|
4975
|
+
};
|
|
4976
|
+
function defineModel(opts) {
|
|
4977
|
+
return {
|
|
4978
|
+
id: opts.id,
|
|
4979
|
+
providerID: PROVIDER_ID,
|
|
4980
|
+
api: { id: opts.id, url: "", npm: NPM },
|
|
4981
|
+
name: `${opts.name} (${opts.multiplier}\xD7)`,
|
|
4982
|
+
family: opts.family,
|
|
4983
|
+
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
4984
|
+
cost: {
|
|
4985
|
+
input: opts.cost.input,
|
|
4986
|
+
output: opts.cost.output,
|
|
4987
|
+
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
4988
|
+
},
|
|
4989
|
+
limit: { context: opts.context, output: opts.output },
|
|
4990
|
+
status: opts.status ?? "active",
|
|
4991
|
+
options: {},
|
|
4992
|
+
headers: {},
|
|
4993
|
+
release_date: opts.releaseDate,
|
|
4994
|
+
variants: opts.reasoning ? reasoningVariants : void 0
|
|
4995
|
+
};
|
|
4937
4996
|
}
|
|
4938
|
-
function
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4997
|
+
function passthroughModel(id) {
|
|
4998
|
+
return {
|
|
4999
|
+
id,
|
|
5000
|
+
providerID: PROVIDER_ID,
|
|
5001
|
+
api: { id, url: "", npm: NPM },
|
|
5002
|
+
name: id,
|
|
5003
|
+
family: "",
|
|
5004
|
+
capabilities: { ...baseCapabilities, reasoning: true },
|
|
5005
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
5006
|
+
limit: { context: 2e5, output: 64e3 },
|
|
5007
|
+
status: "active",
|
|
5008
|
+
options: {},
|
|
5009
|
+
headers: {},
|
|
5010
|
+
release_date: "",
|
|
5011
|
+
variants: reasoningVariants
|
|
5012
|
+
};
|
|
4943
5013
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5014
|
+
var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
|
|
5015
|
+
var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
5016
|
+
var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
5017
|
+
var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
5018
|
+
var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
5019
|
+
var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
5020
|
+
var opus55Cost = { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 };
|
|
5021
|
+
var opus55FastCost = { input: 8, output: 40, cacheRead: 0.4, cacheWrite: 10 };
|
|
5022
|
+
function toConfigModel(model) {
|
|
5023
|
+
const inputMods = [];
|
|
5024
|
+
const outputMods = [];
|
|
5025
|
+
for (const [k, v] of Object.entries(model.capabilities.input)) {
|
|
5026
|
+
if (v) inputMods.push(k);
|
|
4950
5027
|
}
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
if (stats.cacheReadTokens) parts.push(`cache read ${formatTokens(stats.cacheReadTokens)}`);
|
|
4954
|
-
if (stats.cacheWriteTokens) parts.push(`cache write ${formatTokens(stats.cacheWriteTokens)}`);
|
|
4955
|
-
if (stats.permissionDenials?.length) {
|
|
4956
|
-
const count = stats.permissionDenials.length;
|
|
4957
|
-
parts.push(`${count} permission ${count === 1 ? "denial" : "denials"}`);
|
|
5028
|
+
for (const [k, v] of Object.entries(model.capabilities.output)) {
|
|
5029
|
+
if (v) outputMods.push(k);
|
|
4958
5030
|
}
|
|
4959
|
-
if (parts.length === 0) return null;
|
|
4960
|
-
return `${TURN_STATS_MARKER} ${parts.join(" \xB7 ")}`;
|
|
4961
|
-
}
|
|
4962
|
-
function formatTurnStatsBlock(stats) {
|
|
4963
|
-
const line = formatTurnStatsLine(stats);
|
|
4964
|
-
return line === null ? null : `
|
|
4965
|
-
${line}
|
|
4966
|
-
`;
|
|
4967
|
-
}
|
|
4968
|
-
function turnStatsLogPayload(stats) {
|
|
4969
5031
|
return {
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5032
|
+
id: model.api.id,
|
|
5033
|
+
name: model.name,
|
|
5034
|
+
status: model.status,
|
|
5035
|
+
family: model.family ?? "",
|
|
5036
|
+
release_date: model.release_date,
|
|
5037
|
+
temperature: model.capabilities.temperature,
|
|
5038
|
+
reasoning: model.capabilities.reasoning,
|
|
5039
|
+
attachment: model.capabilities.attachment,
|
|
5040
|
+
tool_call: model.capabilities.toolcall,
|
|
5041
|
+
modalities: { input: inputMods, output: outputMods },
|
|
5042
|
+
cost: {
|
|
5043
|
+
input: model.cost.input,
|
|
5044
|
+
output: model.cost.output,
|
|
5045
|
+
cache_read: model.cost.cache.read,
|
|
5046
|
+
cache_write: model.cost.cache.write
|
|
5047
|
+
},
|
|
5048
|
+
limit: model.limit,
|
|
5049
|
+
options: model.options,
|
|
5050
|
+
headers: model.headers,
|
|
5051
|
+
variants: model.variants
|
|
4980
5052
|
};
|
|
4981
5053
|
}
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
}
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
})
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5054
|
+
var defaultModels = {
|
|
5055
|
+
"claude-haiku-4-5": defineModel({
|
|
5056
|
+
id: "claude-haiku-4-5",
|
|
5057
|
+
name: "Claude Haiku 4.5",
|
|
5058
|
+
family: "haiku",
|
|
5059
|
+
reasoning: false,
|
|
5060
|
+
context: 2e5,
|
|
5061
|
+
output: 64e3,
|
|
5062
|
+
cost: haikuCost,
|
|
5063
|
+
multiplier: 1,
|
|
5064
|
+
releaseDate: "2025-10-01"
|
|
5065
|
+
}),
|
|
5066
|
+
"claude-sonnet-4-5": defineModel({
|
|
5067
|
+
id: "claude-sonnet-4-5",
|
|
5068
|
+
name: "Claude Sonnet 4.5",
|
|
5069
|
+
family: "sonnet",
|
|
5070
|
+
reasoning: true,
|
|
5071
|
+
context: 2e5,
|
|
5072
|
+
output: 64e3,
|
|
5073
|
+
cost: sonnetCost,
|
|
5074
|
+
multiplier: 3,
|
|
5075
|
+
releaseDate: "2025-09-29"
|
|
5076
|
+
}),
|
|
5077
|
+
"claude-sonnet-4-6": defineModel({
|
|
5078
|
+
id: "claude-sonnet-4-6",
|
|
5079
|
+
name: "Claude Sonnet 4.6",
|
|
5080
|
+
family: "sonnet",
|
|
5081
|
+
reasoning: true,
|
|
5082
|
+
context: 1e6,
|
|
5083
|
+
output: 128e3,
|
|
5084
|
+
cost: sonnetCost,
|
|
5085
|
+
multiplier: 3,
|
|
5086
|
+
releaseDate: "2025-06-19"
|
|
5087
|
+
}),
|
|
5088
|
+
"claude-sonnet-5": defineModel({
|
|
5089
|
+
id: "claude-sonnet-5",
|
|
5090
|
+
name: "Claude Sonnet 5",
|
|
5091
|
+
family: "sonnet",
|
|
5092
|
+
reasoning: true,
|
|
5093
|
+
context: 1e6,
|
|
5094
|
+
output: 128e3,
|
|
5095
|
+
cost: sonnetCost,
|
|
5096
|
+
multiplier: 3,
|
|
5097
|
+
releaseDate: "2026-06-30"
|
|
5098
|
+
}),
|
|
5099
|
+
"claude-opus-4-5": defineModel({
|
|
5100
|
+
id: "claude-opus-4-5",
|
|
5101
|
+
name: "Claude Opus 4.5",
|
|
5102
|
+
family: "opus",
|
|
5103
|
+
reasoning: true,
|
|
5104
|
+
context: 2e5,
|
|
5105
|
+
output: 64e3,
|
|
5106
|
+
cost: opusCost,
|
|
5107
|
+
multiplier: 5,
|
|
5108
|
+
releaseDate: "2025-11-01"
|
|
5109
|
+
}),
|
|
5110
|
+
"claude-opus-4-6": defineModel({
|
|
5111
|
+
id: "claude-opus-4-6",
|
|
5112
|
+
name: "Claude Opus 4.6",
|
|
5113
|
+
family: "opus",
|
|
5114
|
+
reasoning: true,
|
|
5115
|
+
context: 1e6,
|
|
5116
|
+
output: 128e3,
|
|
5117
|
+
cost: opusCost,
|
|
5118
|
+
multiplier: 5,
|
|
5119
|
+
releaseDate: "2025-06-19"
|
|
5120
|
+
}),
|
|
5121
|
+
"claude-opus-4-7": defineModel({
|
|
5122
|
+
id: "claude-opus-4-7",
|
|
5123
|
+
name: "Claude Opus 4.7",
|
|
5124
|
+
family: "opus",
|
|
5125
|
+
reasoning: true,
|
|
5126
|
+
context: 1e6,
|
|
5127
|
+
output: 128e3,
|
|
5128
|
+
cost: opusCost,
|
|
5129
|
+
multiplier: 5,
|
|
5130
|
+
releaseDate: "2025-07-16"
|
|
5131
|
+
}),
|
|
5132
|
+
"claude-opus-4-8": defineModel({
|
|
5133
|
+
id: "claude-opus-4-8",
|
|
5134
|
+
name: "Claude Opus 4.8",
|
|
5135
|
+
family: "opus",
|
|
5136
|
+
reasoning: true,
|
|
5137
|
+
context: 1e6,
|
|
5138
|
+
output: 128e3,
|
|
5139
|
+
cost: opusCost,
|
|
5140
|
+
multiplier: 5,
|
|
5141
|
+
releaseDate: "2026-05-28"
|
|
5142
|
+
}),
|
|
5143
|
+
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
5144
|
+
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
5145
|
+
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
5146
|
+
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
5147
|
+
//
|
|
5148
|
+
// Only Opus 4.8, Opus 5 and Opus 5.5 qualify: the CLI gates fast mode on
|
|
5149
|
+
// the resolved model name containing `opus-4-8` or `opus-5` (which
|
|
5150
|
+
// `claude-opus-5-5` satisfies too, and 2.1.280's model catalog lists
|
|
5151
|
+
// `fast_mode` for it outright), so registering a fast entry for any other
|
|
5152
|
+
// model would produce a picker option that silently runs at standard speed
|
|
5153
|
+
// while displaying the fast price.
|
|
5154
|
+
"claude-opus-4-8-fast": defineModel({
|
|
5155
|
+
id: "claude-opus-4-8-fast",
|
|
5156
|
+
name: "Claude Opus 4.8 Fast",
|
|
5157
|
+
family: "opus",
|
|
5158
|
+
reasoning: true,
|
|
5159
|
+
context: 1e6,
|
|
5160
|
+
output: 128e3,
|
|
5161
|
+
cost: opusFastCost,
|
|
5162
|
+
multiplier: 10,
|
|
5163
|
+
releaseDate: "2026-05-28"
|
|
5164
|
+
}),
|
|
5165
|
+
"claude-opus-5": defineModel({
|
|
5166
|
+
id: "claude-opus-5",
|
|
5167
|
+
name: "Claude Opus 5",
|
|
5168
|
+
family: "opus",
|
|
5169
|
+
reasoning: true,
|
|
5170
|
+
context: 1e6,
|
|
5171
|
+
output: 128e3,
|
|
5172
|
+
cost: opusCost,
|
|
5173
|
+
multiplier: 5,
|
|
5174
|
+
releaseDate: "2026-07-24"
|
|
5175
|
+
}),
|
|
5176
|
+
"claude-opus-5-fast": defineModel({
|
|
5177
|
+
id: "claude-opus-5-fast",
|
|
5178
|
+
name: "Claude Opus 5 Fast",
|
|
5179
|
+
family: "opus",
|
|
5180
|
+
reasoning: true,
|
|
5181
|
+
context: 1e6,
|
|
5182
|
+
output: 128e3,
|
|
5183
|
+
cost: opusFastCost,
|
|
5184
|
+
multiplier: 10,
|
|
5185
|
+
releaseDate: "2026-07-24"
|
|
5186
|
+
}),
|
|
5187
|
+
// Opus 5.5 needs Claude Code 2.1.280+: the API rejects it from an older CLI
|
|
5188
|
+
// with a 400 naming that floor, which the plugin surfaces as a turn error.
|
|
5189
|
+
// Thinking cannot be switched off for it (the CLI's catalog marks it
|
|
5190
|
+
// `rejects_disabled_thinking`); the plugin never asks for that, so nothing
|
|
5191
|
+
// here has to special-case it.
|
|
5192
|
+
"claude-opus-5-5": defineModel({
|
|
5193
|
+
id: "claude-opus-5-5",
|
|
5194
|
+
name: "Claude Opus 5.5",
|
|
5195
|
+
family: "opus",
|
|
5196
|
+
reasoning: true,
|
|
5197
|
+
context: 1e6,
|
|
5198
|
+
output: 128e3,
|
|
5199
|
+
cost: opus55Cost,
|
|
5200
|
+
multiplier: 4,
|
|
5201
|
+
releaseDate: "2026-09-22"
|
|
5202
|
+
}),
|
|
5203
|
+
"claude-opus-5-5-fast": defineModel({
|
|
5204
|
+
id: "claude-opus-5-5-fast",
|
|
5205
|
+
name: "Claude Opus 5.5 Fast",
|
|
5206
|
+
family: "opus",
|
|
5207
|
+
reasoning: true,
|
|
5208
|
+
context: 1e6,
|
|
5209
|
+
output: 128e3,
|
|
5210
|
+
cost: opus55FastCost,
|
|
5211
|
+
multiplier: 8,
|
|
5212
|
+
releaseDate: "2026-09-22"
|
|
5213
|
+
}),
|
|
5214
|
+
"claude-fable-5": defineModel({
|
|
5215
|
+
id: "claude-fable-5",
|
|
5216
|
+
name: "Claude Fable 5",
|
|
5217
|
+
family: "fable",
|
|
5218
|
+
reasoning: true,
|
|
5219
|
+
context: 1e6,
|
|
5220
|
+
output: 128e3,
|
|
5221
|
+
cost: fableCost,
|
|
5222
|
+
multiplier: 10,
|
|
5223
|
+
releaseDate: "2026-06-09"
|
|
5224
|
+
}),
|
|
5225
|
+
"claude-fable-5-1": defineModel({
|
|
5226
|
+
id: "claude-fable-5-1",
|
|
5227
|
+
name: "Claude Fable 5.1",
|
|
5228
|
+
family: "fable",
|
|
5229
|
+
reasoning: true,
|
|
5230
|
+
context: 1e6,
|
|
5231
|
+
output: 128e3,
|
|
5232
|
+
cost: fable51Cost,
|
|
5233
|
+
multiplier: 10,
|
|
5234
|
+
releaseDate: "2026-09-01"
|
|
5235
|
+
}),
|
|
5236
|
+
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
5237
|
+
// pricing without the safety classifiers; limited availability via Project
|
|
5238
|
+
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
5239
|
+
// they are safe to register unconditionally.
|
|
5240
|
+
"claude-mythos-5": defineModel({
|
|
5241
|
+
id: "claude-mythos-5",
|
|
5242
|
+
name: "Claude Mythos 5",
|
|
5243
|
+
family: "mythos",
|
|
5244
|
+
reasoning: true,
|
|
5245
|
+
context: 1e6,
|
|
5246
|
+
output: 128e3,
|
|
5247
|
+
cost: fableCost,
|
|
5248
|
+
multiplier: 10,
|
|
5249
|
+
releaseDate: "2026-06-09"
|
|
5250
|
+
}),
|
|
5251
|
+
"claude-mythos-5-1": defineModel({
|
|
5252
|
+
id: "claude-mythos-5-1",
|
|
5253
|
+
name: "Claude Mythos 5.1",
|
|
5254
|
+
family: "mythos",
|
|
5255
|
+
reasoning: true,
|
|
5256
|
+
context: 1e6,
|
|
5257
|
+
output: 128e3,
|
|
5258
|
+
cost: fable51Cost,
|
|
5259
|
+
multiplier: 10,
|
|
5260
|
+
releaseDate: "2026-09-01"
|
|
5261
|
+
})
|
|
5262
|
+
};
|
|
5263
|
+
var FAST_SUFFIX = "-fast";
|
|
5264
|
+
function parseModelId(modelId) {
|
|
5265
|
+
const at = modelId.indexOf("@");
|
|
5266
|
+
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
5267
|
+
const account = at === -1 ? "" : modelId.slice(at);
|
|
5268
|
+
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
5269
|
+
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
5270
|
+
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
5099
5271
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
5117
|
-
case "content":
|
|
5118
|
-
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
5119
|
-
if (item?.type === "text") return item.text;
|
|
5120
|
-
return JSON.stringify(item);
|
|
5121
|
-
}).join("\n") : JSON.stringify(value.value);
|
|
5122
|
-
default:
|
|
5123
|
-
return JSON.stringify(value);
|
|
5124
|
-
}
|
|
5272
|
+
|
|
5273
|
+
// src/agent-models.ts
|
|
5274
|
+
var AGENT_DIR_NAMES = ["agents", "agent"];
|
|
5275
|
+
var REASONING_EFFORTS = [
|
|
5276
|
+
"minimal",
|
|
5277
|
+
"low",
|
|
5278
|
+
"medium",
|
|
5279
|
+
"high",
|
|
5280
|
+
"xhigh",
|
|
5281
|
+
"max"
|
|
5282
|
+
];
|
|
5283
|
+
var registry = {};
|
|
5284
|
+
var defaultSubagentModel;
|
|
5285
|
+
var providerFallbackModels = [];
|
|
5286
|
+
function setAgentRegistry(records) {
|
|
5287
|
+
registry = records;
|
|
5125
5288
|
}
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
var MAX_TOOL_INPUT_CHARS = 2e3;
|
|
5129
|
-
function clipWithMarker(text, max) {
|
|
5130
|
-
if (text.length <= max) return text;
|
|
5131
|
-
return `${text.slice(0, max)}
|
|
5132
|
-
\u2026[truncated ${text.length - max} chars]`;
|
|
5289
|
+
function getAgentRegistry() {
|
|
5290
|
+
return registry;
|
|
5133
5291
|
}
|
|
5134
|
-
function
|
|
5135
|
-
|
|
5136
|
-
try {
|
|
5137
|
-
raw = typeof input === "string" ? input : JSON.stringify(input);
|
|
5138
|
-
} catch {
|
|
5139
|
-
raw = String(input);
|
|
5140
|
-
}
|
|
5141
|
-
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
5292
|
+
function setDefaultSubagentModel(model) {
|
|
5293
|
+
defaultSubagentModel = model?.trim() || void 0;
|
|
5142
5294
|
}
|
|
5143
|
-
function
|
|
5144
|
-
|
|
5145
|
-
let toolResultCount = 0;
|
|
5146
|
-
if (typeof msg.content === "string") {
|
|
5147
|
-
return { text: msg.content, toolResultCount: 0 };
|
|
5148
|
-
}
|
|
5149
|
-
if (!Array.isArray(msg.content)) {
|
|
5150
|
-
return { text: "", toolResultCount: 0 };
|
|
5151
|
-
}
|
|
5152
|
-
for (const part of msg.content) {
|
|
5153
|
-
if (!part) continue;
|
|
5154
|
-
switch (part.type) {
|
|
5155
|
-
case "text":
|
|
5156
|
-
if (part.text) lines.push(part.text);
|
|
5157
|
-
break;
|
|
5158
|
-
case "tool-call":
|
|
5159
|
-
lines.push(
|
|
5160
|
-
`[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
|
|
5161
|
-
);
|
|
5162
|
-
break;
|
|
5163
|
-
case "tool-result":
|
|
5164
|
-
toolResultCount++;
|
|
5165
|
-
lines.push(
|
|
5166
|
-
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
5167
|
-
${clipWithMarker(
|
|
5168
|
-
getToolResultText(part),
|
|
5169
|
-
MAX_TOOL_RESULT_CHARS
|
|
5170
|
-
)}`
|
|
5171
|
-
);
|
|
5172
|
-
break;
|
|
5173
|
-
case "image":
|
|
5174
|
-
lines.push(
|
|
5175
|
-
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
5176
|
-
);
|
|
5177
|
-
break;
|
|
5178
|
-
case "file":
|
|
5179
|
-
lines.push(
|
|
5180
|
-
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
5181
|
-
);
|
|
5182
|
-
break;
|
|
5183
|
-
case "reasoning":
|
|
5184
|
-
break;
|
|
5185
|
-
}
|
|
5186
|
-
}
|
|
5187
|
-
return { text: lines.join("\n"), toolResultCount };
|
|
5295
|
+
function getDefaultSubagentModel() {
|
|
5296
|
+
return defaultSubagentModel;
|
|
5188
5297
|
}
|
|
5189
|
-
function
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5298
|
+
function setProviderFallbackModels(models) {
|
|
5299
|
+
providerFallbackModels = models ?? [];
|
|
5300
|
+
}
|
|
5301
|
+
function getProviderFallbackModels() {
|
|
5302
|
+
return providerFallbackModels;
|
|
5303
|
+
}
|
|
5304
|
+
function accountMarker(modelId) {
|
|
5305
|
+
const at = modelId.indexOf("@");
|
|
5306
|
+
return at === -1 ? "" : modelId.slice(at);
|
|
5307
|
+
}
|
|
5308
|
+
function withoutAccountMarker(modelId) {
|
|
5309
|
+
const at = modelId.indexOf("@");
|
|
5310
|
+
return at === -1 ? modelId : modelId.slice(0, at);
|
|
5311
|
+
}
|
|
5312
|
+
function qualifyModelName(wanted, referenceModelId) {
|
|
5313
|
+
const base = withoutAccountMarker(wanted.trim());
|
|
5314
|
+
if (!base || !Object.hasOwn(defaultModels, base)) return null;
|
|
5315
|
+
return `${base}${accountMarker(referenceModelId)}`;
|
|
5316
|
+
}
|
|
5317
|
+
function resolveAgentModel(agent, modelId, overrides) {
|
|
5318
|
+
if (!agent) return modelId;
|
|
5319
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
5320
|
+
if (!record) return modelId;
|
|
5321
|
+
if (record.model?.includes("/")) return modelId;
|
|
5322
|
+
const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
|
|
5323
|
+
const declared = record.forceModel?.trim();
|
|
5324
|
+
const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
|
|
5325
|
+
if (!wanted) return modelId;
|
|
5326
|
+
const resolved = qualifyModelName(wanted, modelId);
|
|
5327
|
+
if (!resolved) {
|
|
5328
|
+
log.warn("agent model override refused: unknown model", {
|
|
5329
|
+
agent,
|
|
5330
|
+
wanted: withoutAccountMarker(wanted),
|
|
5331
|
+
keeping: modelId
|
|
5332
|
+
});
|
|
5333
|
+
return modelId;
|
|
5194
5334
|
}
|
|
5195
|
-
|
|
5196
|
-
(
|
|
5197
|
-
);
|
|
5198
|
-
if (conversationMessages.length <= 1) {
|
|
5199
|
-
return null;
|
|
5335
|
+
if (resolved !== modelId) {
|
|
5336
|
+
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
5200
5337
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5338
|
+
return resolved;
|
|
5339
|
+
}
|
|
5340
|
+
function resolveAgentEffort(agent, inherited, overrides) {
|
|
5341
|
+
if (!agent) return inherited;
|
|
5342
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
5343
|
+
const declared = record?.reasoningEffort?.trim();
|
|
5344
|
+
if (!declared) return inherited;
|
|
5345
|
+
if (!REASONING_EFFORTS.includes(declared)) {
|
|
5346
|
+
log.warn("agent effort override refused: unknown level", {
|
|
5347
|
+
agent,
|
|
5348
|
+
wanted: declared,
|
|
5349
|
+
keeping: inherited
|
|
5350
|
+
});
|
|
5351
|
+
return inherited;
|
|
5210
5352
|
}
|
|
5211
|
-
if (
|
|
5212
|
-
|
|
5353
|
+
if (declared !== inherited) {
|
|
5354
|
+
log.debug("agent effort override", {
|
|
5355
|
+
agent,
|
|
5356
|
+
from: inherited,
|
|
5357
|
+
to: declared
|
|
5358
|
+
});
|
|
5213
5359
|
}
|
|
5214
|
-
return
|
|
5360
|
+
return declared;
|
|
5215
5361
|
}
|
|
5216
|
-
function
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
let
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5362
|
+
function parseAgentFrontmatter(text) {
|
|
5363
|
+
const record = {};
|
|
5364
|
+
if (!text.startsWith("---")) return record;
|
|
5365
|
+
const lines = text.split(/\r?\n/);
|
|
5366
|
+
let listKey = null;
|
|
5367
|
+
for (let i = 1; i < lines.length; i++) {
|
|
5368
|
+
const line = lines[i];
|
|
5369
|
+
if (line.trim() === "---") break;
|
|
5370
|
+
if (listKey) {
|
|
5371
|
+
const item = /^[ \t]*-[ \t]+(.*)$/.exec(line);
|
|
5372
|
+
if (item) {
|
|
5373
|
+
const value2 = item[1].trim().replace(/^["']|["']$/g, "");
|
|
5374
|
+
if (value2) (record[listKey] ??= []).push(value2);
|
|
5375
|
+
continue;
|
|
5376
|
+
}
|
|
5377
|
+
listKey = null;
|
|
5231
5378
|
}
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5379
|
+
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
5380
|
+
if (!match) continue;
|
|
5381
|
+
const key = match[1];
|
|
5382
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
5383
|
+
if (key === "fallbackModels") {
|
|
5384
|
+
if (value) record.fallbackModels = parseFallbackModelList(value);
|
|
5385
|
+
else listKey = "fallbackModels";
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5388
|
+
if (!value) continue;
|
|
5389
|
+
if (key === "mode") record.mode = value;
|
|
5390
|
+
else if (key === "model") record.model = value;
|
|
5391
|
+
else if (key === "forceModel") record.forceModel = value;
|
|
5392
|
+
else if (key === "reasoningEffort") record.reasoningEffort = value;
|
|
5235
5393
|
}
|
|
5236
|
-
|
|
5237
|
-
entries.reverse();
|
|
5238
|
-
log.info("built compaction history", {
|
|
5239
|
-
entries: entries.length,
|
|
5240
|
-
chars: total,
|
|
5241
|
-
toolResults: totalToolResults,
|
|
5242
|
-
droppedOldestBefore: droppedOldest
|
|
5243
|
-
});
|
|
5244
|
-
return entries.join("\n\n");
|
|
5394
|
+
return record;
|
|
5245
5395
|
}
|
|
5246
|
-
function
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
const content = [];
|
|
5250
|
-
prompt = stripAccountFailoverParts(prompt);
|
|
5251
|
-
if (opts.stripContextReminders) {
|
|
5252
|
-
const stripped = stripContextReminders(prompt);
|
|
5253
|
-
if (stripped.removed > 0) {
|
|
5254
|
-
log.info("stripped unsatisfiable context reminders", {
|
|
5255
|
-
blocks: stripped.removed
|
|
5256
|
-
});
|
|
5257
|
-
prompt = stripped.prompt;
|
|
5258
|
-
}
|
|
5396
|
+
function parseFallbackModelList(value) {
|
|
5397
|
+
if (Array.isArray(value)) {
|
|
5398
|
+
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
5259
5399
|
}
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5400
|
+
if (typeof value !== "string") return [];
|
|
5401
|
+
return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
5402
|
+
}
|
|
5403
|
+
async function readAgentMarkdownRecords(directories) {
|
|
5404
|
+
const records = {};
|
|
5405
|
+
for (const directory of directories) {
|
|
5406
|
+
let entries;
|
|
5407
|
+
try {
|
|
5408
|
+
entries = await readdir(directory);
|
|
5409
|
+
} catch {
|
|
5410
|
+
continue;
|
|
5266
5411
|
}
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
const transcript = compactConversationHistory(prompt, {
|
|
5281
|
-
mode: "compaction"
|
|
5282
|
-
});
|
|
5283
|
-
if (transcript) {
|
|
5284
|
-
log.info("including compaction transcript", {
|
|
5285
|
-
historyLength: transcript.length
|
|
5286
|
-
});
|
|
5287
|
-
content.push({
|
|
5288
|
-
type: "text",
|
|
5289
|
-
text: `<conversation_transcript>
|
|
5290
|
-
${transcript}
|
|
5291
|
-
</conversation_transcript>
|
|
5292
|
-
|
|
5293
|
-
The complete prior conversation appears above. The synthesis instructions follow below.
|
|
5294
|
-
|
|
5295
|
-
`
|
|
5296
|
-
});
|
|
5412
|
+
for (const entry of entries) {
|
|
5413
|
+
if (!entry.endsWith(".md")) continue;
|
|
5414
|
+
const name = entry.slice(0, -3);
|
|
5415
|
+
if (records[name]) continue;
|
|
5416
|
+
try {
|
|
5417
|
+
const text = await readFile(path6.join(directory, entry), "utf8");
|
|
5418
|
+
records[name] = parseAgentFrontmatter(text);
|
|
5419
|
+
} catch (err) {
|
|
5420
|
+
log.debug("failed to read agent markdown", {
|
|
5421
|
+
file: path6.join(directory, entry),
|
|
5422
|
+
error: String(err)
|
|
5423
|
+
});
|
|
5424
|
+
}
|
|
5297
5425
|
}
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
text: `<conversation_history>
|
|
5307
|
-
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
5308
|
-
|
|
5309
|
-
${historyContext}
|
|
5310
|
-
|
|
5311
|
-
</conversation_history>
|
|
5312
|
-
|
|
5313
|
-
Now continuing with the current message:
|
|
5314
|
-
|
|
5315
|
-
`
|
|
5316
|
-
});
|
|
5426
|
+
}
|
|
5427
|
+
return records;
|
|
5428
|
+
}
|
|
5429
|
+
function agentDirectories(home, projectDirectory) {
|
|
5430
|
+
const directories = [];
|
|
5431
|
+
if (projectDirectory) {
|
|
5432
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
5433
|
+
directories.push(path6.join(projectDirectory, ".opencode", name));
|
|
5317
5434
|
}
|
|
5318
5435
|
}
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5436
|
+
if (home) {
|
|
5437
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
5438
|
+
directories.push(path6.join(home, ".config", "opencode", name));
|
|
5439
|
+
}
|
|
5323
5440
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
} else if (part.type === "file" || part.type === "image") {
|
|
5339
|
-
const block = toImageBlock(part);
|
|
5340
|
-
if (block) {
|
|
5341
|
-
content.push(block);
|
|
5342
|
-
} else {
|
|
5343
|
-
log.debug("skipped non-image file part", {
|
|
5344
|
-
mediaType: part.mediaType
|
|
5345
|
-
});
|
|
5346
|
-
}
|
|
5347
|
-
} else if (part.type === "tool-result") {
|
|
5348
|
-
pushToolResult(part);
|
|
5349
|
-
}
|
|
5350
|
-
}
|
|
5351
|
-
}
|
|
5352
|
-
} else if (msg.role === "tool") {
|
|
5353
|
-
if (Array.isArray(msg.content)) {
|
|
5354
|
-
for (const part of msg.content) {
|
|
5355
|
-
if (part?.type === "tool-result") {
|
|
5356
|
-
pushToolResult(part);
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
}
|
|
5441
|
+
return directories;
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
// src/model-fallback.ts
|
|
5445
|
+
var MODEL_FALLBACK_MARKER = "\u258C **model fallback:**";
|
|
5446
|
+
var MODEL_REFUSAL_ERROR_KINDS = ["model_not_found"];
|
|
5447
|
+
var MODEL_REFUSAL_PATTERNS = [
|
|
5448
|
+
/There[’'`]?s an issue with the selected model/i
|
|
5449
|
+
];
|
|
5450
|
+
function firstText(content) {
|
|
5451
|
+
if (!Array.isArray(content)) return void 0;
|
|
5452
|
+
for (const block of content) {
|
|
5453
|
+
if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
|
5454
|
+
return block.text.trim();
|
|
5360
5455
|
}
|
|
5361
5456
|
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5457
|
+
return void 0;
|
|
5458
|
+
}
|
|
5459
|
+
function modelRefusalFromAssistant(msg) {
|
|
5460
|
+
if (msg.type !== "assistant" || typeof msg.error !== "string") return null;
|
|
5461
|
+
if (!MODEL_REFUSAL_ERROR_KINDS.includes(msg.error)) return null;
|
|
5462
|
+
return { kind: "model_not_found", detail: firstText(msg.message?.content) };
|
|
5463
|
+
}
|
|
5464
|
+
function modelRefusalFromResult(msg) {
|
|
5465
|
+
if (msg.type !== "result" || msg.is_error !== true) return null;
|
|
5466
|
+
const text = typeof msg.result === "string" ? msg.result : "";
|
|
5467
|
+
if (!text || !MODEL_REFUSAL_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
5468
|
+
return null;
|
|
5371
5469
|
}
|
|
5372
|
-
return
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5470
|
+
return { kind: "model_not_found", detail: text.trim() };
|
|
5471
|
+
}
|
|
5472
|
+
function provesModelServing(msg) {
|
|
5473
|
+
if (msg.type === "content_block_start" || msg.type === "content_block_delta") {
|
|
5474
|
+
return true;
|
|
5475
|
+
}
|
|
5476
|
+
if (msg.type !== "assistant") return false;
|
|
5477
|
+
if (msg.is_api_error_message === true) return false;
|
|
5478
|
+
return Array.isArray(msg.message?.content) && msg.message.content.length > 0;
|
|
5479
|
+
}
|
|
5480
|
+
function resolveFallbackChain(agent, modelId, overrides) {
|
|
5481
|
+
const record = agent ? (overrides?.records ?? getAgentRegistry())[agent] : void 0;
|
|
5482
|
+
if (record?.model?.includes("/")) return [];
|
|
5483
|
+
const declared = record?.fallbackModels?.length ? record.fallbackModels : (overrides ? overrides.providerFallbackModels : getProviderFallbackModels()) ?? [];
|
|
5484
|
+
if (declared.length === 0) return [];
|
|
5485
|
+
const source = record?.fallbackModels?.length ? `agent "${agent}"` : "provider";
|
|
5486
|
+
const chain = [];
|
|
5487
|
+
for (const wanted of declared) {
|
|
5488
|
+
const resolved = qualifyModelName(wanted, modelId);
|
|
5489
|
+
if (!resolved) {
|
|
5490
|
+
log.warn("fallback model refused: unknown model", {
|
|
5491
|
+
source,
|
|
5492
|
+
wanted,
|
|
5493
|
+
modelId
|
|
5494
|
+
});
|
|
5495
|
+
continue;
|
|
5377
5496
|
}
|
|
5378
|
-
|
|
5497
|
+
if (resolved === modelId || chain.includes(resolved)) continue;
|
|
5498
|
+
chain.push(resolved);
|
|
5499
|
+
}
|
|
5500
|
+
return chain;
|
|
5501
|
+
}
|
|
5502
|
+
function nextFallbackModel(chain, tried) {
|
|
5503
|
+
return chain.find((candidate) => !tried.has(candidate));
|
|
5504
|
+
}
|
|
5505
|
+
function describeRefusal(refusal) {
|
|
5506
|
+
return refusal.kind === "account_limit" ? "is out of usage on this account and no other account is configured" : "was refused by the Claude CLI (model_not_found: it is retired, misspelled, or this account cannot use it)";
|
|
5507
|
+
}
|
|
5508
|
+
function formatModelFallbackNote(input) {
|
|
5509
|
+
return `
|
|
5510
|
+
${MODEL_FALLBACK_MARKER} "${input.failed}" ${describeRefusal(
|
|
5511
|
+
input.refusal
|
|
5512
|
+
)}, so this turn is being served by "${input.serving}" instead. The account, the thinking budget and the working directory are unchanged.
|
|
5513
|
+
`;
|
|
5379
5514
|
}
|
|
5380
5515
|
|
|
5381
|
-
// src/
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
}
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
interleaved: false
|
|
5402
|
-
};
|
|
5403
|
-
function defineModel(opts) {
|
|
5404
|
-
return {
|
|
5405
|
-
id: opts.id,
|
|
5406
|
-
providerID: PROVIDER_ID,
|
|
5407
|
-
api: { id: opts.id, url: "", npm: NPM },
|
|
5408
|
-
name: `${opts.name} (${opts.multiplier}\xD7)`,
|
|
5409
|
-
family: opts.family,
|
|
5410
|
-
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
5411
|
-
cost: {
|
|
5412
|
-
input: opts.cost.input,
|
|
5413
|
-
output: opts.cost.output,
|
|
5414
|
-
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
5415
|
-
},
|
|
5416
|
-
limit: { context: opts.context, output: opts.output },
|
|
5417
|
-
status: opts.status ?? "active",
|
|
5418
|
-
options: {},
|
|
5419
|
-
headers: {},
|
|
5420
|
-
release_date: opts.releaseDate,
|
|
5421
|
-
variants: opts.reasoning ? reasoningVariants : void 0
|
|
5422
|
-
};
|
|
5516
|
+
// src/turn-stats.ts
|
|
5517
|
+
var TURN_STATS_MARKER = "\u258C **stats:**";
|
|
5518
|
+
function extractTurnStats(msg) {
|
|
5519
|
+
const usage = msg.usage;
|
|
5520
|
+
const stats = {};
|
|
5521
|
+
if (typeof msg.total_cost_usd === "number") stats.costUsd = msg.total_cost_usd;
|
|
5522
|
+
if (typeof msg.duration_ms === "number") stats.durationMs = msg.duration_ms;
|
|
5523
|
+
if (typeof msg.duration_api_ms === "number") stats.durationApiMs = msg.duration_api_ms;
|
|
5524
|
+
if (typeof msg.num_turns === "number") stats.numTurns = msg.num_turns;
|
|
5525
|
+
if (typeof usage?.input_tokens === "number") stats.inputTokens = usage.input_tokens;
|
|
5526
|
+
if (typeof usage?.output_tokens === "number") stats.outputTokens = usage.output_tokens;
|
|
5527
|
+
if (typeof usage?.cache_read_input_tokens === "number") {
|
|
5528
|
+
stats.cacheReadTokens = usage.cache_read_input_tokens;
|
|
5529
|
+
}
|
|
5530
|
+
if (typeof usage?.cache_creation_input_tokens === "number") {
|
|
5531
|
+
stats.cacheWriteTokens = usage.cache_creation_input_tokens;
|
|
5532
|
+
}
|
|
5533
|
+
if (msg.modelUsage && typeof msg.modelUsage === "object") stats.modelUsage = msg.modelUsage;
|
|
5534
|
+
if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
|
|
5535
|
+
return stats;
|
|
5423
5536
|
}
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5537
|
+
function formatCost(costUsd) {
|
|
5538
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
|
|
5539
|
+
return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
|
|
5540
|
+
}
|
|
5541
|
+
function formatDuration(durationMs) {
|
|
5542
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) return "0.0 s";
|
|
5543
|
+
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)} s`;
|
|
5544
|
+
const totalSeconds = Math.round(durationMs / 1e3);
|
|
5545
|
+
return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
|
|
5546
|
+
}
|
|
5547
|
+
function formatTokens(tokens) {
|
|
5548
|
+
if (!Number.isFinite(tokens) || tokens < 0) return "0";
|
|
5549
|
+
if (tokens < 1e3) return String(Math.round(tokens));
|
|
5550
|
+
if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
5551
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
5552
|
+
}
|
|
5553
|
+
function formatTurnStatsLine(stats) {
|
|
5554
|
+
const parts = [];
|
|
5555
|
+
if (stats.costUsd !== void 0) parts.push(formatCost(stats.costUsd));
|
|
5556
|
+
if (stats.durationMs !== void 0) parts.push(formatDuration(stats.durationMs));
|
|
5557
|
+
if (stats.numTurns !== void 0) {
|
|
5558
|
+
parts.push(`${stats.numTurns} CLI ${stats.numTurns === 1 ? "turn" : "turns"}`);
|
|
5437
5559
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5560
|
+
if (stats.inputTokens !== void 0) parts.push(`in ${formatTokens(stats.inputTokens)}`);
|
|
5561
|
+
if (stats.outputTokens !== void 0) parts.push(`out ${formatTokens(stats.outputTokens)}`);
|
|
5562
|
+
if (stats.cacheReadTokens) parts.push(`cache read ${formatTokens(stats.cacheReadTokens)}`);
|
|
5563
|
+
if (stats.cacheWriteTokens) parts.push(`cache write ${formatTokens(stats.cacheWriteTokens)}`);
|
|
5564
|
+
if (stats.permissionDenials?.length) {
|
|
5565
|
+
const count = stats.permissionDenials.length;
|
|
5566
|
+
parts.push(`${count} permission ${count === 1 ? "denial" : "denials"}`);
|
|
5440
5567
|
}
|
|
5568
|
+
if (parts.length === 0) return null;
|
|
5569
|
+
return `${TURN_STATS_MARKER} ${parts.join(" \xB7 ")}`;
|
|
5570
|
+
}
|
|
5571
|
+
function formatTurnStatsBlock(stats) {
|
|
5572
|
+
const line = formatTurnStatsLine(stats);
|
|
5573
|
+
return line === null ? null : `
|
|
5574
|
+
${line}
|
|
5575
|
+
`;
|
|
5576
|
+
}
|
|
5577
|
+
function turnStatsLogPayload(stats) {
|
|
5441
5578
|
return {
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
cost: {
|
|
5453
|
-
input: model.cost.input,
|
|
5454
|
-
output: model.cost.output,
|
|
5455
|
-
cache_read: model.cost.cache.read,
|
|
5456
|
-
cache_write: model.cost.cache.write
|
|
5457
|
-
},
|
|
5458
|
-
limit: model.limit,
|
|
5459
|
-
options: model.options,
|
|
5460
|
-
headers: model.headers,
|
|
5461
|
-
variants: model.variants
|
|
5579
|
+
costUsd: stats.costUsd ?? null,
|
|
5580
|
+
durationMs: stats.durationMs ?? null,
|
|
5581
|
+
durationApiMs: stats.durationApiMs ?? null,
|
|
5582
|
+
numTurns: stats.numTurns ?? null,
|
|
5583
|
+
inputTokens: stats.inputTokens ?? null,
|
|
5584
|
+
outputTokens: stats.outputTokens ?? null,
|
|
5585
|
+
cacheReadTokens: stats.cacheReadTokens ?? null,
|
|
5586
|
+
cacheWriteTokens: stats.cacheWriteTokens ?? null,
|
|
5587
|
+
modelUsage: stats.modelUsage ?? null,
|
|
5588
|
+
permissionDenials: stats.permissionDenials?.length ?? 0
|
|
5462
5589
|
};
|
|
5463
5590
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
output: 128e3,
|
|
5505
|
-
cost: sonnetCost,
|
|
5506
|
-
multiplier: 3,
|
|
5507
|
-
releaseDate: "2026-06-30"
|
|
5508
|
-
}),
|
|
5509
|
-
"claude-opus-4-5": defineModel({
|
|
5510
|
-
id: "claude-opus-4-5",
|
|
5511
|
-
name: "Claude Opus 4.5",
|
|
5512
|
-
family: "opus",
|
|
5513
|
-
reasoning: true,
|
|
5514
|
-
context: 2e5,
|
|
5515
|
-
output: 64e3,
|
|
5516
|
-
cost: opusCost,
|
|
5517
|
-
multiplier: 5,
|
|
5518
|
-
releaseDate: "2025-11-01"
|
|
5519
|
-
}),
|
|
5520
|
-
"claude-opus-4-6": defineModel({
|
|
5521
|
-
id: "claude-opus-4-6",
|
|
5522
|
-
name: "Claude Opus 4.6",
|
|
5523
|
-
family: "opus",
|
|
5524
|
-
reasoning: true,
|
|
5525
|
-
context: 1e6,
|
|
5526
|
-
output: 128e3,
|
|
5527
|
-
cost: opusCost,
|
|
5528
|
-
multiplier: 5,
|
|
5529
|
-
releaseDate: "2025-06-19"
|
|
5530
|
-
}),
|
|
5531
|
-
"claude-opus-4-7": defineModel({
|
|
5532
|
-
id: "claude-opus-4-7",
|
|
5533
|
-
name: "Claude Opus 4.7",
|
|
5534
|
-
family: "opus",
|
|
5535
|
-
reasoning: true,
|
|
5536
|
-
context: 1e6,
|
|
5537
|
-
output: 128e3,
|
|
5538
|
-
cost: opusCost,
|
|
5539
|
-
multiplier: 5,
|
|
5540
|
-
releaseDate: "2025-07-16"
|
|
5541
|
-
}),
|
|
5542
|
-
"claude-opus-4-8": defineModel({
|
|
5543
|
-
id: "claude-opus-4-8",
|
|
5544
|
-
name: "Claude Opus 4.8",
|
|
5545
|
-
family: "opus",
|
|
5546
|
-
reasoning: true,
|
|
5547
|
-
context: 1e6,
|
|
5548
|
-
output: 128e3,
|
|
5549
|
-
cost: opusCost,
|
|
5550
|
-
multiplier: 5,
|
|
5551
|
-
releaseDate: "2026-05-28"
|
|
5552
|
-
}),
|
|
5553
|
-
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
5554
|
-
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
5555
|
-
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
5556
|
-
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
5557
|
-
//
|
|
5558
|
-
// Only Opus 4.8, Opus 5 and Opus 5.5 qualify: the CLI gates fast mode on
|
|
5559
|
-
// the resolved model name containing `opus-4-8` or `opus-5` (which
|
|
5560
|
-
// `claude-opus-5-5` satisfies too, and 2.1.280's model catalog lists
|
|
5561
|
-
// `fast_mode` for it outright), so registering a fast entry for any other
|
|
5562
|
-
// model would produce a picker option that silently runs at standard speed
|
|
5563
|
-
// while displaying the fast price.
|
|
5564
|
-
"claude-opus-4-8-fast": defineModel({
|
|
5565
|
-
id: "claude-opus-4-8-fast",
|
|
5566
|
-
name: "Claude Opus 4.8 Fast",
|
|
5567
|
-
family: "opus",
|
|
5568
|
-
reasoning: true,
|
|
5569
|
-
context: 1e6,
|
|
5570
|
-
output: 128e3,
|
|
5571
|
-
cost: opusFastCost,
|
|
5572
|
-
multiplier: 10,
|
|
5573
|
-
releaseDate: "2026-05-28"
|
|
5574
|
-
}),
|
|
5575
|
-
"claude-opus-5": defineModel({
|
|
5576
|
-
id: "claude-opus-5",
|
|
5577
|
-
name: "Claude Opus 5",
|
|
5578
|
-
family: "opus",
|
|
5579
|
-
reasoning: true,
|
|
5580
|
-
context: 1e6,
|
|
5581
|
-
output: 128e3,
|
|
5582
|
-
cost: opusCost,
|
|
5583
|
-
multiplier: 5,
|
|
5584
|
-
releaseDate: "2026-07-24"
|
|
5585
|
-
}),
|
|
5586
|
-
"claude-opus-5-fast": defineModel({
|
|
5587
|
-
id: "claude-opus-5-fast",
|
|
5588
|
-
name: "Claude Opus 5 Fast",
|
|
5589
|
-
family: "opus",
|
|
5590
|
-
reasoning: true,
|
|
5591
|
-
context: 1e6,
|
|
5592
|
-
output: 128e3,
|
|
5593
|
-
cost: opusFastCost,
|
|
5594
|
-
multiplier: 10,
|
|
5595
|
-
releaseDate: "2026-07-24"
|
|
5596
|
-
}),
|
|
5597
|
-
// Opus 5.5 needs Claude Code 2.1.280+: the API rejects it from an older CLI
|
|
5598
|
-
// with a 400 naming that floor, which the plugin surfaces as a turn error.
|
|
5599
|
-
// Thinking cannot be switched off for it (the CLI's catalog marks it
|
|
5600
|
-
// `rejects_disabled_thinking`); the plugin never asks for that, so nothing
|
|
5601
|
-
// here has to special-case it.
|
|
5602
|
-
"claude-opus-5-5": defineModel({
|
|
5603
|
-
id: "claude-opus-5-5",
|
|
5604
|
-
name: "Claude Opus 5.5",
|
|
5605
|
-
family: "opus",
|
|
5606
|
-
reasoning: true,
|
|
5607
|
-
context: 1e6,
|
|
5608
|
-
output: 128e3,
|
|
5609
|
-
cost: opus55Cost,
|
|
5610
|
-
multiplier: 4,
|
|
5611
|
-
releaseDate: "2026-09-22"
|
|
5612
|
-
}),
|
|
5613
|
-
"claude-opus-5-5-fast": defineModel({
|
|
5614
|
-
id: "claude-opus-5-5-fast",
|
|
5615
|
-
name: "Claude Opus 5.5 Fast",
|
|
5616
|
-
family: "opus",
|
|
5617
|
-
reasoning: true,
|
|
5618
|
-
context: 1e6,
|
|
5619
|
-
output: 128e3,
|
|
5620
|
-
cost: opus55FastCost,
|
|
5621
|
-
multiplier: 8,
|
|
5622
|
-
releaseDate: "2026-09-22"
|
|
5623
|
-
}),
|
|
5624
|
-
"claude-fable-5": defineModel({
|
|
5625
|
-
id: "claude-fable-5",
|
|
5626
|
-
name: "Claude Fable 5",
|
|
5627
|
-
family: "fable",
|
|
5628
|
-
reasoning: true,
|
|
5629
|
-
context: 1e6,
|
|
5630
|
-
output: 128e3,
|
|
5631
|
-
cost: fableCost,
|
|
5632
|
-
multiplier: 10,
|
|
5633
|
-
releaseDate: "2026-06-09"
|
|
5634
|
-
}),
|
|
5635
|
-
"claude-fable-5-1": defineModel({
|
|
5636
|
-
id: "claude-fable-5-1",
|
|
5637
|
-
name: "Claude Fable 5.1",
|
|
5638
|
-
family: "fable",
|
|
5639
|
-
reasoning: true,
|
|
5640
|
-
context: 1e6,
|
|
5641
|
-
output: 128e3,
|
|
5642
|
-
cost: fable51Cost,
|
|
5643
|
-
multiplier: 10,
|
|
5644
|
-
releaseDate: "2026-09-01"
|
|
5645
|
-
}),
|
|
5646
|
-
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
5647
|
-
// pricing without the safety classifiers; limited availability via Project
|
|
5648
|
-
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
5649
|
-
// they are safe to register unconditionally.
|
|
5650
|
-
"claude-mythos-5": defineModel({
|
|
5651
|
-
id: "claude-mythos-5",
|
|
5652
|
-
name: "Claude Mythos 5",
|
|
5653
|
-
family: "mythos",
|
|
5654
|
-
reasoning: true,
|
|
5655
|
-
context: 1e6,
|
|
5656
|
-
output: 128e3,
|
|
5657
|
-
cost: fableCost,
|
|
5658
|
-
multiplier: 10,
|
|
5659
|
-
releaseDate: "2026-06-09"
|
|
5660
|
-
}),
|
|
5661
|
-
"claude-mythos-5-1": defineModel({
|
|
5662
|
-
id: "claude-mythos-5-1",
|
|
5663
|
-
name: "Claude Mythos 5.1",
|
|
5664
|
-
family: "mythos",
|
|
5665
|
-
reasoning: true,
|
|
5666
|
-
context: 1e6,
|
|
5667
|
-
output: 128e3,
|
|
5668
|
-
cost: fable51Cost,
|
|
5669
|
-
multiplier: 10,
|
|
5670
|
-
releaseDate: "2026-09-01"
|
|
5671
|
-
})
|
|
5672
|
-
};
|
|
5673
|
-
var FAST_SUFFIX = "-fast";
|
|
5674
|
-
function parseModelId(modelId) {
|
|
5675
|
-
const at = modelId.indexOf("@");
|
|
5676
|
-
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
5677
|
-
const account = at === -1 ? "" : modelId.slice(at);
|
|
5678
|
-
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
5679
|
-
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
5680
|
-
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
5591
|
+
|
|
5592
|
+
// src/message-builder.ts
|
|
5593
|
+
var PLUGIN_NOTE_MARKERS = [
|
|
5594
|
+
INLINE_ASIDE_MARKER,
|
|
5595
|
+
...LEGACY_INLINE_ASIDE_MARKERS,
|
|
5596
|
+
TURN_STATS_MARKER,
|
|
5597
|
+
COMPACT_BOUNDARY_MARKER,
|
|
5598
|
+
CONVERSATION_RESET_MARKER,
|
|
5599
|
+
RATE_LIMIT_MARKER,
|
|
5600
|
+
RESULT_ERROR_MARKER,
|
|
5601
|
+
DOCTOR_MARKER,
|
|
5602
|
+
STREAM_TIMEOUT_MARKER,
|
|
5603
|
+
SILENT_TURN_MARKER,
|
|
5604
|
+
FAILOVER_MARKER,
|
|
5605
|
+
ACCOUNT_BLOCK_MARKER,
|
|
5606
|
+
MODEL_FALLBACK_MARKER
|
|
5607
|
+
];
|
|
5608
|
+
function isPluginNote(part) {
|
|
5609
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
5610
|
+
const text = part.text.trimStart();
|
|
5611
|
+
return PLUGIN_NOTE_MARKERS.some((marker) => text.startsWith(marker));
|
|
5612
|
+
}
|
|
5613
|
+
function stripPluginNotes(content) {
|
|
5614
|
+
if (!Array.isArray(content)) return content;
|
|
5615
|
+
const kept = content.filter((part) => !isPluginNote(part));
|
|
5616
|
+
return kept.length === content.length ? content : kept;
|
|
5617
|
+
}
|
|
5618
|
+
function filterSideQuestionHistory(prompt) {
|
|
5619
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
5620
|
+
let pluginCommand = false;
|
|
5621
|
+
const kept = prompt.filter((message) => {
|
|
5622
|
+
if (message.role === "user") {
|
|
5623
|
+
pluginCommand = parseSideQuestionContent(message.content) !== null || parseDoctorCommandContent(message.content) !== null;
|
|
5624
|
+
return !pluginCommand;
|
|
5625
|
+
}
|
|
5626
|
+
return message.role !== "assistant" || !pluginCommand;
|
|
5627
|
+
});
|
|
5628
|
+
return kept.map(
|
|
5629
|
+
(message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
|
|
5630
|
+
);
|
|
5681
5631
|
}
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
"minimal",
|
|
5687
|
-
"low",
|
|
5688
|
-
"medium",
|
|
5689
|
-
"high",
|
|
5690
|
-
"xhigh",
|
|
5691
|
-
"max"
|
|
5692
|
-
];
|
|
5693
|
-
var registry = {};
|
|
5694
|
-
var defaultSubagentModel;
|
|
5695
|
-
function setAgentRegistry(records) {
|
|
5696
|
-
registry = records;
|
|
5632
|
+
var DCP_REMINDER_BLOCK = /<dcp-system-reminder\b[^>]*>[\s\S]*?<\/dcp-system-reminder>/gi;
|
|
5633
|
+
function stripContextReminderBlocks(text) {
|
|
5634
|
+
if (!text.includes("<dcp-system-reminder")) return text;
|
|
5635
|
+
return text.replace(DCP_REMINDER_BLOCK, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
5697
5636
|
}
|
|
5698
|
-
function
|
|
5699
|
-
return
|
|
5637
|
+
function shouldStripContextReminders(options) {
|
|
5638
|
+
if (options.enabled !== true) return false;
|
|
5639
|
+
const namesCompress = (list2) => (list2 ?? []).some((name) => String(name).trim().toLowerCase() === "compress");
|
|
5640
|
+
return !namesCompress(options.proxyTools) && !namesCompress(options.proxyOpencodeTools);
|
|
5700
5641
|
}
|
|
5701
|
-
function
|
|
5702
|
-
|
|
5642
|
+
function stripContextReminders(prompt) {
|
|
5643
|
+
let removed = 0;
|
|
5644
|
+
const countIn = (text) => (text.match(DCP_REMINDER_BLOCK) ?? []).length;
|
|
5645
|
+
const out = prompt.map((message) => {
|
|
5646
|
+
if (message.role !== "user" && message.role !== "assistant") return message;
|
|
5647
|
+
if (!Array.isArray(message.content)) return message;
|
|
5648
|
+
let touched = false;
|
|
5649
|
+
const parts = message.content.map((part) => {
|
|
5650
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return part;
|
|
5651
|
+
const hits = countIn(part.text);
|
|
5652
|
+
if (hits === 0) return part;
|
|
5653
|
+
removed += hits;
|
|
5654
|
+
touched = true;
|
|
5655
|
+
return { ...part, text: stripContextReminderBlocks(part.text) };
|
|
5656
|
+
});
|
|
5657
|
+
return touched ? { ...message, content: parts } : message;
|
|
5658
|
+
});
|
|
5659
|
+
return removed > 0 ? { prompt: out, removed } : { prompt, removed: 0 };
|
|
5703
5660
|
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5661
|
+
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
5662
|
+
"image/jpeg",
|
|
5663
|
+
"image/png",
|
|
5664
|
+
"image/gif",
|
|
5665
|
+
"image/webp"
|
|
5666
|
+
]);
|
|
5667
|
+
function toImageBlock(part) {
|
|
5668
|
+
const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
|
|
5669
|
+
if (!raw) {
|
|
5670
|
+
log.warn("file part without data, skipping");
|
|
5671
|
+
return null;
|
|
5672
|
+
}
|
|
5673
|
+
let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
|
|
5674
|
+
let base64 = null;
|
|
5675
|
+
if (typeof raw === "string") {
|
|
5676
|
+
if (raw.startsWith("data:")) {
|
|
5677
|
+
const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
|
|
5678
|
+
if (!match) {
|
|
5679
|
+
log.warn("malformed data URI, skipping file part");
|
|
5680
|
+
return null;
|
|
5681
|
+
}
|
|
5682
|
+
resolvedMediaType = resolvedMediaType || match[1];
|
|
5683
|
+
base64 = match[2];
|
|
5684
|
+
} else if (/^https?:\/\//i.test(raw)) {
|
|
5685
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
5686
|
+
return null;
|
|
5687
|
+
} else {
|
|
5688
|
+
base64 = raw;
|
|
5689
|
+
}
|
|
5690
|
+
} else if (raw instanceof URL) {
|
|
5691
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
5692
|
+
return null;
|
|
5693
|
+
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
5694
|
+
base64 = Buffer.from(raw).toString("base64");
|
|
5695
|
+
} else {
|
|
5696
|
+
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
5697
|
+
return null;
|
|
5698
|
+
}
|
|
5699
|
+
if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
|
|
5700
|
+
log.warn("unsupported media type for Claude image block, skipping", {
|
|
5701
|
+
mediaType: resolvedMediaType
|
|
5702
|
+
});
|
|
5703
|
+
return null;
|
|
5704
|
+
}
|
|
5705
|
+
return {
|
|
5706
|
+
type: "image",
|
|
5707
|
+
source: { type: "base64", media_type: resolvedMediaType, data: base64 }
|
|
5708
|
+
};
|
|
5706
5709
|
}
|
|
5707
|
-
function
|
|
5708
|
-
const
|
|
5709
|
-
|
|
5710
|
+
function getToolResultText(part) {
|
|
5711
|
+
const value = part.output ?? part.result;
|
|
5712
|
+
if (typeof value === "string") {
|
|
5713
|
+
return value;
|
|
5714
|
+
}
|
|
5715
|
+
if (!value || typeof value !== "object") {
|
|
5716
|
+
return JSON.stringify(value);
|
|
5717
|
+
}
|
|
5718
|
+
switch (value.type) {
|
|
5719
|
+
case "text":
|
|
5720
|
+
case "error-text":
|
|
5721
|
+
return String(value.value);
|
|
5722
|
+
case "json":
|
|
5723
|
+
case "error-json":
|
|
5724
|
+
return JSON.stringify(value.value);
|
|
5725
|
+
case "execution-denied":
|
|
5726
|
+
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
5727
|
+
case "content":
|
|
5728
|
+
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
5729
|
+
if (item?.type === "text") return item.text;
|
|
5730
|
+
return JSON.stringify(item);
|
|
5731
|
+
}).join("\n") : JSON.stringify(value.value);
|
|
5732
|
+
default:
|
|
5733
|
+
return JSON.stringify(value);
|
|
5734
|
+
}
|
|
5710
5735
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5736
|
+
var MAX_HISTORY_CHARS = 18e4;
|
|
5737
|
+
var MAX_TOOL_RESULT_CHARS = 1e4;
|
|
5738
|
+
var MAX_TOOL_INPUT_CHARS = 2e3;
|
|
5739
|
+
function clipWithMarker(text, max) {
|
|
5740
|
+
if (text.length <= max) return text;
|
|
5741
|
+
return `${text.slice(0, max)}
|
|
5742
|
+
\u2026[truncated ${text.length - max} chars]`;
|
|
5714
5743
|
}
|
|
5715
|
-
function
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
const declared = record.forceModel?.trim();
|
|
5722
|
-
const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
|
|
5723
|
-
if (!wanted) return modelId;
|
|
5724
|
-
const base = withoutAccountMarker(wanted);
|
|
5725
|
-
if (!Object.hasOwn(defaultModels, base)) {
|
|
5726
|
-
log.warn("agent model override refused: unknown model", {
|
|
5727
|
-
agent,
|
|
5728
|
-
wanted: base,
|
|
5729
|
-
keeping: modelId
|
|
5730
|
-
});
|
|
5731
|
-
return modelId;
|
|
5744
|
+
function renderToolInput(input) {
|
|
5745
|
+
let raw;
|
|
5746
|
+
try {
|
|
5747
|
+
raw = typeof input === "string" ? input : JSON.stringify(input);
|
|
5748
|
+
} catch {
|
|
5749
|
+
raw = String(input);
|
|
5732
5750
|
}
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5751
|
+
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
5752
|
+
}
|
|
5753
|
+
function renderMessageContentForCompaction(msg) {
|
|
5754
|
+
const lines = [];
|
|
5755
|
+
let toolResultCount = 0;
|
|
5756
|
+
if (typeof msg.content === "string") {
|
|
5757
|
+
return { text: msg.content, toolResultCount: 0 };
|
|
5736
5758
|
}
|
|
5737
|
-
|
|
5759
|
+
if (!Array.isArray(msg.content)) {
|
|
5760
|
+
return { text: "", toolResultCount: 0 };
|
|
5761
|
+
}
|
|
5762
|
+
for (const part of msg.content) {
|
|
5763
|
+
if (!part) continue;
|
|
5764
|
+
switch (part.type) {
|
|
5765
|
+
case "text":
|
|
5766
|
+
if (part.text) lines.push(part.text);
|
|
5767
|
+
break;
|
|
5768
|
+
case "tool-call":
|
|
5769
|
+
lines.push(
|
|
5770
|
+
`[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
|
|
5771
|
+
);
|
|
5772
|
+
break;
|
|
5773
|
+
case "tool-result":
|
|
5774
|
+
toolResultCount++;
|
|
5775
|
+
lines.push(
|
|
5776
|
+
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
5777
|
+
${clipWithMarker(
|
|
5778
|
+
getToolResultText(part),
|
|
5779
|
+
MAX_TOOL_RESULT_CHARS
|
|
5780
|
+
)}`
|
|
5781
|
+
);
|
|
5782
|
+
break;
|
|
5783
|
+
case "image":
|
|
5784
|
+
lines.push(
|
|
5785
|
+
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
5786
|
+
);
|
|
5787
|
+
break;
|
|
5788
|
+
case "file":
|
|
5789
|
+
lines.push(
|
|
5790
|
+
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
5791
|
+
);
|
|
5792
|
+
break;
|
|
5793
|
+
case "reasoning":
|
|
5794
|
+
break;
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
return { text: lines.join("\n"), toolResultCount };
|
|
5798
|
+
}
|
|
5799
|
+
function compactConversationHistory(prompt, opts = {}) {
|
|
5800
|
+
const mode = opts.mode ?? "fresh-session";
|
|
5801
|
+
prompt = filterSideQuestionHistory(prompt);
|
|
5802
|
+
if (mode === "compaction") {
|
|
5803
|
+
return buildCompactionHistory(prompt);
|
|
5804
|
+
}
|
|
5805
|
+
const conversationMessages = prompt.filter(
|
|
5806
|
+
(m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
|
|
5807
|
+
);
|
|
5808
|
+
if (conversationMessages.length <= 1) {
|
|
5809
|
+
return null;
|
|
5810
|
+
}
|
|
5811
|
+
const historyParts = [];
|
|
5812
|
+
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
5813
|
+
const msg = conversationMessages[i];
|
|
5814
|
+
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
5815
|
+
const { text } = renderMessageContentForCompaction(msg);
|
|
5816
|
+
if (text.trim()) {
|
|
5817
|
+
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
5818
|
+
historyParts.push(`${role}: ${truncated}`);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
if (historyParts.length === 0) {
|
|
5822
|
+
return null;
|
|
5823
|
+
}
|
|
5824
|
+
return historyParts.join("\n\n");
|
|
5825
|
+
}
|
|
5826
|
+
function buildCompactionHistory(prompt) {
|
|
5827
|
+
const entries = [];
|
|
5828
|
+
let total = 0;
|
|
5829
|
+
let totalToolResults = 0;
|
|
5830
|
+
let droppedOldest = 0;
|
|
5831
|
+
const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
|
|
5832
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
5833
|
+
const msg = prompt[i];
|
|
5834
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
|
|
5835
|
+
const { text, toolResultCount } = renderMessageContentForCompaction(msg);
|
|
5836
|
+
if (!text.trim()) continue;
|
|
5837
|
+
const entry = `${roleLabel}: ${text}`;
|
|
5838
|
+
if (total + entry.length > MAX_HISTORY_CHARS) {
|
|
5839
|
+
droppedOldest = i + 1;
|
|
5840
|
+
break;
|
|
5841
|
+
}
|
|
5842
|
+
entries.push(entry);
|
|
5843
|
+
total += entry.length + 2;
|
|
5844
|
+
totalToolResults += toolResultCount;
|
|
5845
|
+
}
|
|
5846
|
+
if (entries.length === 0) return null;
|
|
5847
|
+
entries.reverse();
|
|
5848
|
+
log.info("built compaction history", {
|
|
5849
|
+
entries: entries.length,
|
|
5850
|
+
chars: total,
|
|
5851
|
+
toolResults: totalToolResults,
|
|
5852
|
+
droppedOldestBefore: droppedOldest
|
|
5853
|
+
});
|
|
5854
|
+
return entries.join("\n\n");
|
|
5738
5855
|
}
|
|
5739
|
-
function
|
|
5740
|
-
|
|
5741
|
-
const
|
|
5742
|
-
const
|
|
5743
|
-
|
|
5744
|
-
if (
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5856
|
+
function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
|
|
5857
|
+
const compactionMode = opts.compactionMode === true;
|
|
5858
|
+
const cliToolCallIds = opts.cliToolCallIds;
|
|
5859
|
+
const content = [];
|
|
5860
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
5861
|
+
if (opts.stripContextReminders) {
|
|
5862
|
+
const stripped = stripContextReminders(prompt);
|
|
5863
|
+
if (stripped.removed > 0) {
|
|
5864
|
+
log.info("stripped unsatisfiable context reminders", {
|
|
5865
|
+
blocks: stripped.removed
|
|
5866
|
+
});
|
|
5867
|
+
prompt = stripped.prompt;
|
|
5868
|
+
}
|
|
5751
5869
|
}
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5870
|
+
const pushToolResult = (part) => {
|
|
5871
|
+
const id = part.toolCallId;
|
|
5872
|
+
const text = getToolResultText(part);
|
|
5873
|
+
if (!cliToolCallIds || cliToolCallIds.has(id)) {
|
|
5874
|
+
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
log.info("rendering opencode-side tool result as text", {
|
|
5878
|
+
toolCallId: id,
|
|
5879
|
+
toolName: part.toolName,
|
|
5880
|
+
chars: text.length
|
|
5881
|
+
});
|
|
5882
|
+
content.push({
|
|
5883
|
+
type: "text",
|
|
5884
|
+
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
5885
|
+
${text}
|
|
5886
|
+
</opencode_tool_result>`
|
|
5887
|
+
});
|
|
5888
|
+
};
|
|
5889
|
+
if (compactionMode) {
|
|
5890
|
+
const transcript = compactConversationHistory(prompt, {
|
|
5891
|
+
mode: "compaction"
|
|
5757
5892
|
});
|
|
5893
|
+
if (transcript) {
|
|
5894
|
+
log.info("including compaction transcript", {
|
|
5895
|
+
historyLength: transcript.length
|
|
5896
|
+
});
|
|
5897
|
+
content.push({
|
|
5898
|
+
type: "text",
|
|
5899
|
+
text: `<conversation_transcript>
|
|
5900
|
+
${transcript}
|
|
5901
|
+
</conversation_transcript>
|
|
5902
|
+
|
|
5903
|
+
The complete prior conversation appears above. The synthesis instructions follow below.
|
|
5904
|
+
|
|
5905
|
+
`
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
} else if (includeHistoryContext) {
|
|
5909
|
+
const historyContext = compactConversationHistory(prompt);
|
|
5910
|
+
if (historyContext) {
|
|
5911
|
+
log.info("including conversation history context", {
|
|
5912
|
+
historyLength: historyContext.length
|
|
5913
|
+
});
|
|
5914
|
+
content.push({
|
|
5915
|
+
type: "text",
|
|
5916
|
+
text: `<conversation_history>
|
|
5917
|
+
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
5918
|
+
|
|
5919
|
+
${historyContext}
|
|
5920
|
+
|
|
5921
|
+
</conversation_history>
|
|
5922
|
+
|
|
5923
|
+
Now continuing with the current message:
|
|
5924
|
+
|
|
5925
|
+
`
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5758
5928
|
}
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
if (!text.startsWith("---")) return record;
|
|
5764
|
-
const lines = text.split(/\r?\n/);
|
|
5765
|
-
for (let i = 1; i < lines.length; i++) {
|
|
5766
|
-
const line = lines[i];
|
|
5767
|
-
if (line.trim() === "---") break;
|
|
5768
|
-
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
5769
|
-
if (!match) continue;
|
|
5770
|
-
const key = match[1];
|
|
5771
|
-
if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
|
|
5772
|
-
continue;
|
|
5773
|
-
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
5774
|
-
if (value) record[key] = value;
|
|
5929
|
+
const messages = [];
|
|
5930
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
5931
|
+
if (prompt[i].role === "assistant") break;
|
|
5932
|
+
messages.unshift(prompt[i]);
|
|
5775
5933
|
}
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5934
|
+
for (const msg of messages) {
|
|
5935
|
+
if (msg.role === "user") {
|
|
5936
|
+
if (parseSideQuestionContent(msg.content) !== null) continue;
|
|
5937
|
+
if (typeof msg.content === "string") {
|
|
5938
|
+
const str2 = msg.content;
|
|
5939
|
+
if (str2.trim()) {
|
|
5940
|
+
content.push({ type: "text", text: str2 });
|
|
5941
|
+
}
|
|
5942
|
+
} else if (Array.isArray(msg.content)) {
|
|
5943
|
+
for (const part of msg.content) {
|
|
5944
|
+
if (part.type === "text") {
|
|
5945
|
+
if (part.text && part.text.trim()) {
|
|
5946
|
+
content.push({ type: "text", text: part.text });
|
|
5947
|
+
}
|
|
5948
|
+
} else if (part.type === "file" || part.type === "image") {
|
|
5949
|
+
const block = toImageBlock(part);
|
|
5950
|
+
if (block) {
|
|
5951
|
+
content.push(block);
|
|
5952
|
+
} else {
|
|
5953
|
+
log.debug("skipped non-image file part", {
|
|
5954
|
+
mediaType: part.mediaType
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
} else if (part.type === "tool-result") {
|
|
5958
|
+
pushToolResult(part);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
} else if (msg.role === "tool") {
|
|
5963
|
+
if (Array.isArray(msg.content)) {
|
|
5964
|
+
for (const part of msg.content) {
|
|
5965
|
+
if (part?.type === "tool-result") {
|
|
5966
|
+
pushToolResult(part);
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5799
5969
|
}
|
|
5800
5970
|
}
|
|
5801
5971
|
}
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5972
|
+
if (content.length === 0) {
|
|
5973
|
+
log.warn("empty user content; sending sentinel to satisfy CLI");
|
|
5974
|
+
return JSON.stringify({
|
|
5975
|
+
type: "user",
|
|
5976
|
+
message: {
|
|
5977
|
+
role: "user",
|
|
5978
|
+
content: [{ type: "text", text: "(empty)" }]
|
|
5979
|
+
}
|
|
5980
|
+
});
|
|
5810
5981
|
}
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5982
|
+
return JSON.stringify({
|
|
5983
|
+
type: "user",
|
|
5984
|
+
message: {
|
|
5985
|
+
role: "user",
|
|
5986
|
+
content
|
|
5814
5987
|
}
|
|
5815
|
-
}
|
|
5816
|
-
return directories;
|
|
5988
|
+
});
|
|
5817
5989
|
}
|
|
5818
5990
|
|
|
5819
5991
|
// src/skill-bridge.ts
|
|
@@ -8310,23 +8482,133 @@ ${plan}
|
|
|
8310
8482
|
* returned untouched.
|
|
8311
8483
|
*/
|
|
8312
8484
|
async doStream(options) {
|
|
8313
|
-
const result = await this.
|
|
8485
|
+
const result = await this.runModelChain(options);
|
|
8314
8486
|
return {
|
|
8315
8487
|
...result,
|
|
8316
8488
|
stream: translateStreamForHost(result.stream, this.config.hostApi ?? "v1")
|
|
8317
8489
|
};
|
|
8318
8490
|
}
|
|
8319
|
-
|
|
8491
|
+
/**
|
|
8492
|
+
* Run the turn, moving to the next model in the fallback chain if the one
|
|
8493
|
+
* it started on is refused. The single wiring point for `src/model-fallback.ts`.
|
|
8494
|
+
*
|
|
8495
|
+
* With no chain declared (the default) this is `doStreamForHost` and one
|
|
8496
|
+
* `await`, so nothing about an existing install changes. With a chain, one
|
|
8497
|
+
* rule carries the whole design: **an attempt's parts are withheld until it
|
|
8498
|
+
* proves the model is serving**, and a refused attempt is then discarded
|
|
8499
|
+
* whole rather than edited. That is what keeps the CLI's "There's an issue
|
|
8500
|
+
* with the selected model" text out of the operator's transcript and out of
|
|
8501
|
+
* any replay, and it costs nothing on a served turn, because the very first
|
|
8502
|
+
* content block commits the attempt and every later part passes straight
|
|
8503
|
+
* through.
|
|
8504
|
+
*
|
|
8505
|
+
* The bound is `tried`: one turn spawns each model at most once, in order,
|
|
8506
|
+
* and an exhausted chain leaves the last attempt un-armed so its error
|
|
8507
|
+
* surfaces exactly as it does today.
|
|
8508
|
+
*/
|
|
8509
|
+
async runModelChain(options) {
|
|
8510
|
+
const agent = this.getOpencodeAgent(options);
|
|
8511
|
+
const chain = this.isCompactionCall(options) ? [] : resolveFallbackChain(agent, resolveAgentModel(agent, this.modelId));
|
|
8512
|
+
if (chain.length === 0) return this.doStreamForHost(options);
|
|
8513
|
+
const self = this;
|
|
8514
|
+
const tried = /* @__PURE__ */ new Set();
|
|
8515
|
+
let attempt = { armed: true };
|
|
8516
|
+
let inner = await this.doStreamForHost(options, attempt);
|
|
8517
|
+
const request = inner.request;
|
|
8518
|
+
const stream = new ReadableStream({
|
|
8519
|
+
async start(controller) {
|
|
8520
|
+
let pendingNote = null;
|
|
8521
|
+
try {
|
|
8522
|
+
for (; ; ) {
|
|
8523
|
+
if (attempt.modelId) tried.add(attempt.modelId);
|
|
8524
|
+
const buffered = [];
|
|
8525
|
+
let flushed = false;
|
|
8526
|
+
const emitNote = (note) => {
|
|
8527
|
+
const id = generateId();
|
|
8528
|
+
controller.enqueue({ type: "text-start", id });
|
|
8529
|
+
controller.enqueue({ type: "text-delta", id, delta: note });
|
|
8530
|
+
controller.enqueue({ type: "text-end", id });
|
|
8531
|
+
};
|
|
8532
|
+
const flush = () => {
|
|
8533
|
+
if (flushed) return;
|
|
8534
|
+
flushed = true;
|
|
8535
|
+
for (const part of buffered) {
|
|
8536
|
+
controller.enqueue(part);
|
|
8537
|
+
if (pendingNote && part.type === "stream-start") {
|
|
8538
|
+
emitNote(pendingNote);
|
|
8539
|
+
pendingNote = null;
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
buffered.length = 0;
|
|
8543
|
+
if (pendingNote) {
|
|
8544
|
+
emitNote(pendingNote);
|
|
8545
|
+
pendingNote = null;
|
|
8546
|
+
}
|
|
8547
|
+
};
|
|
8548
|
+
const reader = inner.stream.getReader();
|
|
8549
|
+
try {
|
|
8550
|
+
for (; ; ) {
|
|
8551
|
+
const { done, value } = await reader.read();
|
|
8552
|
+
if (done) break;
|
|
8553
|
+
if (flushed) {
|
|
8554
|
+
controller.enqueue(value);
|
|
8555
|
+
continue;
|
|
8556
|
+
}
|
|
8557
|
+
buffered.push(value);
|
|
8558
|
+
if (attempt.serving) flush();
|
|
8559
|
+
}
|
|
8560
|
+
} finally {
|
|
8561
|
+
reader.releaseLock();
|
|
8562
|
+
}
|
|
8563
|
+
const refusal = attempt.refusal;
|
|
8564
|
+
const next = refusal && !attempt.serving ? nextFallbackModel(chain, tried) : void 0;
|
|
8565
|
+
if (!next || !refusal) {
|
|
8566
|
+
flush();
|
|
8567
|
+
break;
|
|
8568
|
+
}
|
|
8569
|
+
buffered.length = 0;
|
|
8570
|
+
pendingNote = formatModelFallbackNote({
|
|
8571
|
+
failed: attempt.modelId ?? self.modelId,
|
|
8572
|
+
serving: next,
|
|
8573
|
+
refusal
|
|
8574
|
+
});
|
|
8575
|
+
log.notice("falling back to the next model in the chain", {
|
|
8576
|
+
from: attempt.modelId ?? self.modelId,
|
|
8577
|
+
to: next,
|
|
8578
|
+
reason: refusal.kind,
|
|
8579
|
+
tried: [...tried]
|
|
8580
|
+
});
|
|
8581
|
+
tried.add(next);
|
|
8582
|
+
attempt = {
|
|
8583
|
+
modelOverride: next,
|
|
8584
|
+
armed: nextFallbackModel(chain, tried) !== void 0
|
|
8585
|
+
};
|
|
8586
|
+
inner = await self.doStreamForHost(options, attempt);
|
|
8587
|
+
}
|
|
8588
|
+
} catch (error) {
|
|
8589
|
+
controller.enqueue({ type: "error", error });
|
|
8590
|
+
} finally {
|
|
8591
|
+
try {
|
|
8592
|
+
controller.close();
|
|
8593
|
+
} catch {
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8597
|
+
});
|
|
8598
|
+
return { stream, request };
|
|
8599
|
+
}
|
|
8600
|
+
async doStreamForHost(options, attempt) {
|
|
8320
8601
|
const warnings = [];
|
|
8321
8602
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
8322
8603
|
const scope = this.requestScope(options);
|
|
8323
8604
|
const affinity = this.sessionAffinity(options);
|
|
8324
8605
|
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
8325
8606
|
const compactionMode = this.isCompactionCall(options);
|
|
8326
|
-
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
8607
|
+
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : attempt?.modelOverride ?? resolveAgentModel(
|
|
8327
8608
|
this.getOpencodeAgent(options),
|
|
8328
8609
|
this.modelId
|
|
8329
8610
|
);
|
|
8611
|
+
if (attempt) attempt.modelId = effectiveModelId;
|
|
8330
8612
|
const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
|
|
8331
8613
|
this.getOpencodeAgent(options),
|
|
8332
8614
|
this.getReasoningEffort(options.providerOptions)
|
|
@@ -8343,6 +8625,7 @@ ${plan}
|
|
|
8343
8625
|
const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
|
|
8344
8626
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
8345
8627
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
8628
|
+
const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
|
|
8346
8629
|
const sourceAccount = normalizeAccountName(
|
|
8347
8630
|
this.config.account ?? DEFAULT_ACCOUNT
|
|
8348
8631
|
);
|
|
@@ -9138,6 +9421,7 @@ ${plan}
|
|
|
9138
9421
|
let resultFailure;
|
|
9139
9422
|
let accountLimitHit = null;
|
|
9140
9423
|
let accountBlock = null;
|
|
9424
|
+
let modelRefusal = null;
|
|
9141
9425
|
const drainBuffer = [];
|
|
9142
9426
|
let drainTimer = null;
|
|
9143
9427
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -9323,6 +9607,33 @@ ${plan}
|
|
|
9323
9607
|
finishWithQuestionCall(call);
|
|
9324
9608
|
return;
|
|
9325
9609
|
}
|
|
9610
|
+
if (modelFallbackArmed && attempt && msg.is_error === true) {
|
|
9611
|
+
const refusal = modelRefusal ?? (accountLimitHit && !failoverAskActive && !accountBlock ? { kind: "account_limit" } : null);
|
|
9612
|
+
if (refusal) {
|
|
9613
|
+
attempt.refusal = refusal;
|
|
9614
|
+
log.warn(
|
|
9615
|
+
`Claude will not serve "${effectiveModelId}" (${refusal.kind}); falling back to the next model in the chain.`,
|
|
9616
|
+
{ sessionKey: sk, model: effectiveModelId, detail: refusal.detail ?? null }
|
|
9617
|
+
);
|
|
9618
|
+
controller.enqueue({
|
|
9619
|
+
type: "finish",
|
|
9620
|
+
finishReason: { unified: "error", raw: refusal.kind },
|
|
9621
|
+
usage: toUsage2(msg.usage),
|
|
9622
|
+
providerMetadata: {
|
|
9623
|
+
"claude-code": { ...resultMeta, path: "model-fallback" }
|
|
9624
|
+
}
|
|
9625
|
+
});
|
|
9626
|
+
controllerClosed = true;
|
|
9627
|
+
cleanupTurn();
|
|
9628
|
+
deleteActiveProcess(sk);
|
|
9629
|
+
deleteClaudeSessionId(sk);
|
|
9630
|
+
try {
|
|
9631
|
+
controller.close();
|
|
9632
|
+
} catch {
|
|
9633
|
+
}
|
|
9634
|
+
return;
|
|
9635
|
+
}
|
|
9636
|
+
}
|
|
9326
9637
|
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
9327
9638
|
autoContinueState,
|
|
9328
9639
|
{
|
|
@@ -9449,6 +9760,12 @@ ${plan}
|
|
|
9449
9760
|
clearStartWatchdog();
|
|
9450
9761
|
startResultFallback();
|
|
9451
9762
|
}
|
|
9763
|
+
if (attempt && !attempt.serving && provesModelServing(msg)) {
|
|
9764
|
+
attempt.serving = true;
|
|
9765
|
+
}
|
|
9766
|
+
if (modelFallbackArmed && !modelRefusal) {
|
|
9767
|
+
modelRefusal = modelRefusalFromAssistant(msg);
|
|
9768
|
+
}
|
|
9452
9769
|
if (outer.type === "stream_event") {
|
|
9453
9770
|
gotPartialEvents = true;
|
|
9454
9771
|
}
|
|
@@ -10021,6 +10338,9 @@ ${plan}
|
|
|
10021
10338
|
})) {
|
|
10022
10339
|
accountLimitHit = {};
|
|
10023
10340
|
}
|
|
10341
|
+
if (modelFallbackArmed && !modelRefusal) {
|
|
10342
|
+
modelRefusal = modelRefusalFromResult(msg);
|
|
10343
|
+
}
|
|
10024
10344
|
if (accountBlock && msg.is_error) {
|
|
10025
10345
|
resultFailure ??= accountBlock;
|
|
10026
10346
|
const offeringSwitch = failoverAskActive;
|
|
@@ -11049,13 +11369,6 @@ function pickOpencodeDirectory(input) {
|
|
|
11049
11369
|
}
|
|
11050
11370
|
var warnedAnthropicApiKey = false;
|
|
11051
11371
|
var warnedPlanModeNoExit = false;
|
|
11052
|
-
var DEFAULT_PROXY_TOOL_NAMES = [
|
|
11053
|
-
"Bash",
|
|
11054
|
-
"Edit",
|
|
11055
|
-
"Write",
|
|
11056
|
-
"WebFetch",
|
|
11057
|
-
"Task"
|
|
11058
|
-
];
|
|
11059
11372
|
function registerSideQuestionCommand(config) {
|
|
11060
11373
|
config.command ??= {};
|
|
11061
11374
|
if (config.command.btw) return false;
|
|
@@ -11085,7 +11398,7 @@ function warnIfAnthropicApiKey(ignore) {
|
|
|
11085
11398
|
);
|
|
11086
11399
|
} else {
|
|
11087
11400
|
log.warn(
|
|
11088
|
-
"ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; claude may bill as pay-as-you-go API usage instead of your subscription
|
|
11401
|
+
"ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; claude may bill as pay-as-you-go API usage instead of your subscription. Set provider option `ignoreAnthropicApiKey: true` to force subscription auth."
|
|
11089
11402
|
);
|
|
11090
11403
|
}
|
|
11091
11404
|
}
|
|
@@ -11256,11 +11569,35 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
|
|
|
11256
11569
|
}
|
|
11257
11570
|
for (const [id, model] of Object.entries(providerModels)) {
|
|
11258
11571
|
if (!(id in models)) {
|
|
11259
|
-
models[id] =
|
|
11572
|
+
models[id] = completeUserModel(id, model, providerID);
|
|
11260
11573
|
}
|
|
11261
11574
|
}
|
|
11262
11575
|
return models;
|
|
11263
11576
|
}
|
|
11577
|
+
function isRecord4(value) {
|
|
11578
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11579
|
+
}
|
|
11580
|
+
function completeUserModel(id, entry, providerID) {
|
|
11581
|
+
const record = isRecord4(entry) ? entry : {};
|
|
11582
|
+
const runtimeShaped = isRecord4(record.capabilities) && isRecord4(record.capabilities.input) && isRecord4(record.capabilities.output) && isRecord4(record.cost) && isRecord4(record.cost.cache) && isRecord4(record.api);
|
|
11583
|
+
if (runtimeShaped) {
|
|
11584
|
+
return toConfigModel({ ...record, providerID });
|
|
11585
|
+
}
|
|
11586
|
+
const base = toConfigModel({ ...passthroughModel(id), providerID });
|
|
11587
|
+
log.warn("provider model entry is partial; completing it from the pass-through template", {
|
|
11588
|
+
providerID,
|
|
11589
|
+
model: id,
|
|
11590
|
+
declared: Object.keys(record)
|
|
11591
|
+
});
|
|
11592
|
+
const merged = { ...base, ...record, id };
|
|
11593
|
+
for (const key of ["cost", "limit", "variants"]) {
|
|
11594
|
+
merged[key] = {
|
|
11595
|
+
...isRecord4(base[key]) ? base[key] : {},
|
|
11596
|
+
...isRecord4(record[key]) ? record[key] : {}
|
|
11597
|
+
};
|
|
11598
|
+
}
|
|
11599
|
+
return merged;
|
|
11600
|
+
}
|
|
11264
11601
|
async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefaults = {}, displayName) {
|
|
11265
11602
|
const mergedOptions = {
|
|
11266
11603
|
cliPath: "claude",
|
|
@@ -11348,6 +11685,7 @@ async function buildAgentRegistry(config) {
|
|
|
11348
11685
|
setDefaultSubagentModel(
|
|
11349
11686
|
typeof configured === "string" ? configured : void 0
|
|
11350
11687
|
);
|
|
11688
|
+
setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
|
|
11351
11689
|
const records = await readAgentMarkdownRecords(
|
|
11352
11690
|
agentDirectories(
|
|
11353
11691
|
process.env.HOME ?? process.env.USERPROFILE,
|
|
@@ -11360,11 +11698,15 @@ async function buildAgentRegistry(config) {
|
|
|
11360
11698
|
const value = agent[key] ?? bag[key];
|
|
11361
11699
|
return typeof value === "string" ? value : void 0;
|
|
11362
11700
|
};
|
|
11701
|
+
const declaredChain = parseFallbackModelList(
|
|
11702
|
+
agent.fallbackModels ?? bag.fallbackModels
|
|
11703
|
+
);
|
|
11363
11704
|
records[name] = {
|
|
11364
11705
|
mode: pick("mode") ?? records[name]?.mode,
|
|
11365
11706
|
model: pick("model") ?? records[name]?.model,
|
|
11366
11707
|
forceModel: pick("forceModel") ?? records[name]?.forceModel,
|
|
11367
|
-
reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort
|
|
11708
|
+
reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort,
|
|
11709
|
+
fallbackModels: declaredChain.length ? declaredChain : records[name]?.fallbackModels
|
|
11368
11710
|
};
|
|
11369
11711
|
}
|
|
11370
11712
|
setAgentRegistry(records);
|