@khalilgharbaoui/opencode-claude-code-plugin 0.28.0 → 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/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's Agent SDK credit.`;
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/turn-stats.ts
4908
- var TURN_STATS_MARKER = "\u258C **stats:**";
4909
- function extractTurnStats(msg) {
4910
- const usage = msg.usage;
4911
- const stats = {};
4912
- if (typeof msg.total_cost_usd === "number") stats.costUsd = msg.total_cost_usd;
4913
- if (typeof msg.duration_ms === "number") stats.durationMs = msg.duration_ms;
4914
- if (typeof msg.duration_api_ms === "number") stats.durationApiMs = msg.duration_api_ms;
4915
- if (typeof msg.num_turns === "number") stats.numTurns = msg.num_turns;
4916
- if (typeof usage?.input_tokens === "number") stats.inputTokens = usage.input_tokens;
4917
- if (typeof usage?.output_tokens === "number") stats.outputTokens = usage.output_tokens;
4918
- if (typeof usage?.cache_read_input_tokens === "number") {
4919
- stats.cacheReadTokens = usage.cache_read_input_tokens;
4920
- }
4921
- if (typeof usage?.cache_creation_input_tokens === "number") {
4922
- stats.cacheWriteTokens = usage.cache_creation_input_tokens;
4923
- }
4924
- if (msg.modelUsage && typeof msg.modelUsage === "object") stats.modelUsage = msg.modelUsage;
4925
- if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
4926
- return stats;
4927
- }
4928
- function formatCost(costUsd) {
4929
- if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
4930
- return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
4931
- }
4932
- function formatDuration(durationMs) {
4933
- if (!Number.isFinite(durationMs) || durationMs < 0) return "0.0 s";
4934
- if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)} s`;
4935
- const totalSeconds = Math.round(durationMs / 1e3);
4936
- return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
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 formatTokens(tokens) {
4939
- if (!Number.isFinite(tokens) || tokens < 0) return "0";
4940
- if (tokens < 1e3) return String(Math.round(tokens));
4941
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
4942
- return `${(tokens / 1e6).toFixed(1)}M`;
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
- function formatTurnStatsLine(stats) {
4945
- const parts = [];
4946
- if (stats.costUsd !== void 0) parts.push(formatCost(stats.costUsd));
4947
- if (stats.durationMs !== void 0) parts.push(formatDuration(stats.durationMs));
4948
- if (stats.numTurns !== void 0) {
4949
- parts.push(`${stats.numTurns} CLI ${stats.numTurns === 1 ? "turn" : "turns"}`);
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
- if (stats.inputTokens !== void 0) parts.push(`in ${formatTokens(stats.inputTokens)}`);
4952
- if (stats.outputTokens !== void 0) parts.push(`out ${formatTokens(stats.outputTokens)}`);
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
- costUsd: stats.costUsd ?? null,
4971
- durationMs: stats.durationMs ?? null,
4972
- durationApiMs: stats.durationApiMs ?? null,
4973
- numTurns: stats.numTurns ?? null,
4974
- inputTokens: stats.inputTokens ?? null,
4975
- outputTokens: stats.outputTokens ?? null,
4976
- cacheReadTokens: stats.cacheReadTokens ?? null,
4977
- cacheWriteTokens: stats.cacheWriteTokens ?? null,
4978
- modelUsage: stats.modelUsage ?? null,
4979
- permissionDenials: stats.permissionDenials?.length ?? 0
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
- // src/message-builder.ts
4984
- var PLUGIN_NOTE_MARKERS = [
4985
- INLINE_ASIDE_MARKER,
4986
- ...LEGACY_INLINE_ASIDE_MARKERS,
4987
- TURN_STATS_MARKER,
4988
- COMPACT_BOUNDARY_MARKER,
4989
- CONVERSATION_RESET_MARKER,
4990
- RATE_LIMIT_MARKER,
4991
- RESULT_ERROR_MARKER,
4992
- DOCTOR_MARKER,
4993
- STREAM_TIMEOUT_MARKER,
4994
- SILENT_TURN_MARKER,
4995
- FAILOVER_MARKER,
4996
- ACCOUNT_BLOCK_MARKER
4997
- ];
4998
- function isPluginNote(part) {
4999
- if (!part || part.type !== "text" || typeof part.text !== "string") return false;
5000
- const text = part.text.trimStart();
5001
- return PLUGIN_NOTE_MARKERS.some((marker) => text.startsWith(marker));
5002
- }
5003
- function stripPluginNotes(content) {
5004
- if (!Array.isArray(content)) return content;
5005
- const kept = content.filter((part) => !isPluginNote(part));
5006
- return kept.length === content.length ? content : kept;
5007
- }
5008
- function filterSideQuestionHistory(prompt) {
5009
- prompt = stripAccountFailoverParts(prompt);
5010
- let pluginCommand = false;
5011
- const kept = prompt.filter((message) => {
5012
- if (message.role === "user") {
5013
- pluginCommand = parseSideQuestionContent(message.content) !== null || parseDoctorCommandContent(message.content) !== null;
5014
- return !pluginCommand;
5015
- }
5016
- return message.role !== "assistant" || !pluginCommand;
5017
- });
5018
- return kept.map(
5019
- (message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
5020
- );
5021
- }
5022
- var DCP_REMINDER_BLOCK = /<dcp-system-reminder\b[^>]*>[\s\S]*?<\/dcp-system-reminder>/gi;
5023
- function stripContextReminderBlocks(text) {
5024
- if (!text.includes("<dcp-system-reminder")) return text;
5025
- return text.replace(DCP_REMINDER_BLOCK, "").replace(/\n{3,}/g, "\n\n").trim();
5026
- }
5027
- function shouldStripContextReminders(options) {
5028
- if (options.enabled !== true) return false;
5029
- const namesCompress = (list2) => (list2 ?? []).some((name) => String(name).trim().toLowerCase() === "compress");
5030
- return !namesCompress(options.proxyTools) && !namesCompress(options.proxyOpencodeTools);
5031
- }
5032
- function stripContextReminders(prompt) {
5033
- let removed = 0;
5034
- const countIn = (text) => (text.match(DCP_REMINDER_BLOCK) ?? []).length;
5035
- const out = prompt.map((message) => {
5036
- if (message.role !== "user" && message.role !== "assistant") return message;
5037
- if (!Array.isArray(message.content)) return message;
5038
- let touched = false;
5039
- const parts = message.content.map((part) => {
5040
- if (!part || part.type !== "text" || typeof part.text !== "string") return part;
5041
- const hits = countIn(part.text);
5042
- if (hits === 0) return part;
5043
- removed += hits;
5044
- touched = true;
5045
- return { ...part, text: stripContextReminderBlocks(part.text) };
5046
- });
5047
- return touched ? { ...message, content: parts } : message;
5048
- });
5049
- return removed > 0 ? { prompt: out, removed } : { prompt, removed: 0 };
5050
- }
5051
- var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
5052
- "image/jpeg",
5053
- "image/png",
5054
- "image/gif",
5055
- "image/webp"
5056
- ]);
5057
- function toImageBlock(part) {
5058
- const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
5059
- if (!raw) {
5060
- log.warn("file part without data, skipping");
5061
- return null;
5062
- }
5063
- let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
5064
- let base64 = null;
5065
- if (typeof raw === "string") {
5066
- if (raw.startsWith("data:")) {
5067
- const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
5068
- if (!match) {
5069
- log.warn("malformed data URI, skipping file part");
5070
- return null;
5071
- }
5072
- resolvedMediaType = resolvedMediaType || match[1];
5073
- base64 = match[2];
5074
- } else if (/^https?:\/\//i.test(raw)) {
5075
- log.warn("remote URL images are not supported by Claude CLI, skipping");
5076
- return null;
5077
- } else {
5078
- base64 = raw;
5079
- }
5080
- } else if (raw instanceof URL) {
5081
- log.warn("remote URL images are not supported by Claude CLI, skipping");
5082
- return null;
5083
- } else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
5084
- base64 = Buffer.from(raw).toString("base64");
5085
- } else {
5086
- log.warn("unsupported file part data type", { dataType: typeof raw });
5087
- return null;
5088
- }
5089
- if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
5090
- log.warn("unsupported media type for Claude image block, skipping", {
5091
- mediaType: resolvedMediaType
5092
- });
5093
- return null;
5094
- }
5095
- return {
5096
- type: "image",
5097
- source: { type: "base64", media_type: resolvedMediaType, data: base64 }
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
- function getToolResultText(part) {
5101
- const value = part.output ?? part.result;
5102
- if (typeof value === "string") {
5103
- return value;
5104
- }
5105
- if (!value || typeof value !== "object") {
5106
- return JSON.stringify(value);
5107
- }
5108
- switch (value.type) {
5109
- case "text":
5110
- case "error-text":
5111
- return String(value.value);
5112
- case "json":
5113
- case "error-json":
5114
- return JSON.stringify(value.value);
5115
- case "execution-denied":
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
- var MAX_HISTORY_CHARS = 18e4;
5127
- var MAX_TOOL_RESULT_CHARS = 1e4;
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 renderToolInput(input) {
5135
- let raw;
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 renderMessageContentForCompaction(msg) {
5144
- const lines = [];
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 compactConversationHistory(prompt, opts = {}) {
5190
- const mode = opts.mode ?? "fresh-session";
5191
- prompt = filterSideQuestionHistory(prompt);
5192
- if (mode === "compaction") {
5193
- return buildCompactionHistory(prompt);
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
- const conversationMessages = prompt.filter(
5196
- (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
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
- const historyParts = [];
5202
- for (let i = 0; i < conversationMessages.length - 1; i++) {
5203
- const msg = conversationMessages[i];
5204
- const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
5205
- const { text } = renderMessageContentForCompaction(msg);
5206
- if (text.trim()) {
5207
- const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
5208
- historyParts.push(`${role}: ${truncated}`);
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 (historyParts.length === 0) {
5212
- return null;
5353
+ if (declared !== inherited) {
5354
+ log.debug("agent effort override", {
5355
+ agent,
5356
+ from: inherited,
5357
+ to: declared
5358
+ });
5213
5359
  }
5214
- return historyParts.join("\n\n");
5360
+ return declared;
5215
5361
  }
5216
- function buildCompactionHistory(prompt) {
5217
- const entries = [];
5218
- let total = 0;
5219
- let totalToolResults = 0;
5220
- let droppedOldest = 0;
5221
- const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
5222
- for (let i = end - 1; i >= 0; i--) {
5223
- const msg = prompt[i];
5224
- const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
5225
- const { text, toolResultCount } = renderMessageContentForCompaction(msg);
5226
- if (!text.trim()) continue;
5227
- const entry = `${roleLabel}: ${text}`;
5228
- if (total + entry.length > MAX_HISTORY_CHARS) {
5229
- droppedOldest = i + 1;
5230
- break;
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
- entries.push(entry);
5233
- total += entry.length + 2;
5234
- totalToolResults += toolResultCount;
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
- if (entries.length === 0) return null;
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 getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
5247
- const compactionMode = opts.compactionMode === true;
5248
- const cliToolCallIds = opts.cliToolCallIds;
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
- const pushToolResult = (part) => {
5261
- const id = part.toolCallId;
5262
- const text = getToolResultText(part);
5263
- if (!cliToolCallIds || cliToolCallIds.has(id)) {
5264
- content.push({ type: "tool_result", tool_use_id: id, content: text });
5265
- return;
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
- log.info("rendering opencode-side tool result as text", {
5268
- toolCallId: id,
5269
- toolName: part.toolName,
5270
- chars: text.length
5271
- });
5272
- content.push({
5273
- type: "text",
5274
- text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
5275
- ${text}
5276
- </opencode_tool_result>`
5277
- });
5278
- };
5279
- if (compactionMode) {
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
- } else if (includeHistoryContext) {
5299
- const historyContext = compactConversationHistory(prompt);
5300
- if (historyContext) {
5301
- log.info("including conversation history context", {
5302
- historyLength: historyContext.length
5303
- });
5304
- content.push({
5305
- type: "text",
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
- const messages = [];
5320
- for (let i = prompt.length - 1; i >= 0; i--) {
5321
- if (prompt[i].role === "assistant") break;
5322
- messages.unshift(prompt[i]);
5436
+ if (home) {
5437
+ for (const name of AGENT_DIR_NAMES) {
5438
+ directories.push(path6.join(home, ".config", "opencode", name));
5439
+ }
5323
5440
  }
5324
- for (const msg of messages) {
5325
- if (msg.role === "user") {
5326
- if (parseSideQuestionContent(msg.content) !== null) continue;
5327
- if (typeof msg.content === "string") {
5328
- const str2 = msg.content;
5329
- if (str2.trim()) {
5330
- content.push({ type: "text", text: str2 });
5331
- }
5332
- } else if (Array.isArray(msg.content)) {
5333
- for (const part of msg.content) {
5334
- if (part.type === "text") {
5335
- if (part.text && part.text.trim()) {
5336
- content.push({ type: "text", text: part.text });
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
- if (content.length === 0) {
5363
- log.warn("empty user content; sending sentinel to satisfy CLI");
5364
- return JSON.stringify({
5365
- type: "user",
5366
- message: {
5367
- role: "user",
5368
- content: [{ type: "text", text: "(empty)" }]
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 JSON.stringify({
5373
- type: "user",
5374
- message: {
5375
- role: "user",
5376
- content
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/agent-models.ts
5382
- import { readFile, readdir } from "fs/promises";
5383
- import path6 from "path";
5384
-
5385
- // src/models.ts
5386
- var PROVIDER_ID = "claude-code";
5387
- var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
5388
- var reasoningVariants = {
5389
- low: { reasoningEffort: "low" },
5390
- medium: { reasoningEffort: "medium" },
5391
- high: { reasoningEffort: "high" },
5392
- xhigh: { reasoningEffort: "xhigh" },
5393
- max: { reasoningEffort: "max" }
5394
- };
5395
- var baseCapabilities = {
5396
- temperature: false,
5397
- attachment: true,
5398
- toolcall: true,
5399
- input: { text: true, audio: false, image: true, video: false, pdf: false },
5400
- output: { text: true, audio: false, image: false, video: false, pdf: false },
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
- var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
5425
- var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
5426
- var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
5427
- var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
5428
- var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
5429
- var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
5430
- var opus55Cost = { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 };
5431
- var opus55FastCost = { input: 8, output: 40, cacheRead: 0.4, cacheWrite: 10 };
5432
- function toConfigModel(model) {
5433
- const inputMods = [];
5434
- const outputMods = [];
5435
- for (const [k, v] of Object.entries(model.capabilities.input)) {
5436
- if (v) inputMods.push(k);
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
- for (const [k, v] of Object.entries(model.capabilities.output)) {
5439
- if (v) outputMods.push(k);
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
- id: model.api.id,
5443
- name: model.name,
5444
- status: model.status,
5445
- family: model.family ?? "",
5446
- release_date: model.release_date,
5447
- temperature: model.capabilities.temperature,
5448
- reasoning: model.capabilities.reasoning,
5449
- attachment: model.capabilities.attachment,
5450
- tool_call: model.capabilities.toolcall,
5451
- modalities: { input: inputMods, output: outputMods },
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
- var defaultModels = {
5465
- "claude-haiku-4-5": defineModel({
5466
- id: "claude-haiku-4-5",
5467
- name: "Claude Haiku 4.5",
5468
- family: "haiku",
5469
- reasoning: false,
5470
- context: 2e5,
5471
- output: 64e3,
5472
- cost: haikuCost,
5473
- multiplier: 1,
5474
- releaseDate: "2025-10-01"
5475
- }),
5476
- "claude-sonnet-4-5": defineModel({
5477
- id: "claude-sonnet-4-5",
5478
- name: "Claude Sonnet 4.5",
5479
- family: "sonnet",
5480
- reasoning: true,
5481
- context: 2e5,
5482
- output: 64e3,
5483
- cost: sonnetCost,
5484
- multiplier: 3,
5485
- releaseDate: "2025-09-29"
5486
- }),
5487
- "claude-sonnet-4-6": defineModel({
5488
- id: "claude-sonnet-4-6",
5489
- name: "Claude Sonnet 4.6",
5490
- family: "sonnet",
5491
- reasoning: true,
5492
- context: 1e6,
5493
- output: 128e3,
5494
- cost: sonnetCost,
5495
- multiplier: 3,
5496
- releaseDate: "2025-06-19"
5497
- }),
5498
- "claude-sonnet-5": defineModel({
5499
- id: "claude-sonnet-5",
5500
- name: "Claude Sonnet 5",
5501
- family: "sonnet",
5502
- reasoning: true,
5503
- context: 1e6,
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
- // src/agent-models.ts
5684
- var AGENT_DIR_NAMES = ["agents", "agent"];
5685
- var REASONING_EFFORTS = [
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 getAgentRegistry() {
5699
- return registry;
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 setDefaultSubagentModel(model) {
5702
- defaultSubagentModel = model?.trim() || void 0;
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
- function getDefaultSubagentModel() {
5705
- return defaultSubagentModel;
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 accountMarker(modelId) {
5708
- const at = modelId.indexOf("@");
5709
- return at === -1 ? "" : modelId.slice(at);
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
- function withoutAccountMarker(modelId) {
5712
- const at = modelId.indexOf("@");
5713
- return at === -1 ? modelId : modelId.slice(0, at);
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 resolveAgentModel(agent, modelId, overrides) {
5716
- if (!agent) return modelId;
5717
- const record = (overrides?.records ?? registry)[agent];
5718
- if (!record) return modelId;
5719
- if (record.model?.includes("/")) return modelId;
5720
- const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
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
- const resolved = `${base}${accountMarker(modelId)}`;
5734
- if (resolved !== modelId) {
5735
- log.debug("agent model override", { agent, from: modelId, to: resolved });
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
- return resolved;
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 resolveAgentEffort(agent, inherited, overrides) {
5740
- if (!agent) return inherited;
5741
- const record = (overrides?.records ?? registry)[agent];
5742
- const declared = record?.reasoningEffort?.trim();
5743
- if (!declared) return inherited;
5744
- if (!REASONING_EFFORTS.includes(declared)) {
5745
- log.warn("agent effort override refused: unknown level", {
5746
- agent,
5747
- wanted: declared,
5748
- keeping: inherited
5749
- });
5750
- return inherited;
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
- if (declared !== inherited) {
5753
- log.debug("agent effort override", {
5754
- agent,
5755
- from: inherited,
5756
- to: declared
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
- return declared;
5760
- }
5761
- function parseAgentFrontmatter(text) {
5762
- const record = {};
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
- return record;
5777
- }
5778
- async function readAgentMarkdownRecords(directories) {
5779
- const records = {};
5780
- for (const directory of directories) {
5781
- let entries;
5782
- try {
5783
- entries = await readdir(directory);
5784
- } catch {
5785
- continue;
5786
- }
5787
- for (const entry of entries) {
5788
- if (!entry.endsWith(".md")) continue;
5789
- const name = entry.slice(0, -3);
5790
- if (records[name]) continue;
5791
- try {
5792
- const text = await readFile(path6.join(directory, entry), "utf8");
5793
- records[name] = parseAgentFrontmatter(text);
5794
- } catch (err) {
5795
- log.debug("failed to read agent markdown", {
5796
- file: path6.join(directory, entry),
5797
- error: String(err)
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
- return records;
5803
- }
5804
- function agentDirectories(home, projectDirectory) {
5805
- const directories = [];
5806
- if (projectDirectory) {
5807
- for (const name of AGENT_DIR_NAMES) {
5808
- directories.push(path6.join(projectDirectory, ".opencode", name));
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
- if (home) {
5812
- for (const name of AGENT_DIR_NAMES) {
5813
- directories.push(path6.join(home, ".config", "opencode", name));
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.doStreamForHost(options);
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
- async doStreamForHost(options) {
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
  }
@@ -9710,7 +10027,7 @@ ${plan}
9710
10027
  if (!skip) {
9711
10028
  toolCallsById.set(tc.id, {
9712
10029
  id: tc.id,
9713
- name: tc.name,
10030
+ name: mappedName,
9714
10031
  input: parsedInput
9715
10032
  });
9716
10033
  if (!executed) skipResultForIds.add(tc.id);
@@ -9892,7 +10209,7 @@ ${plan}
9892
10209
  if (!skip) {
9893
10210
  toolCallsById.set(block2.id, {
9894
10211
  id: block2.id,
9895
- name: block2.name,
10212
+ name: mappedName,
9896
10213
  input: parsedInput
9897
10214
  });
9898
10215
  if (!executed) skipResultForIds.add(block2.id);
@@ -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 / Agent SDK credit. Set provider option `ignoreAnthropicApiKey: true` to force subscription auth."
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] = toConfigModel({ ...model, providerID });
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);