@smart-coders-hq/opencode-model-fallback 1.0.4 → 1.0.6
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.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +926 -928
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/display/notifier.d.ts +1 -1
- package/dist/src/display/notifier.d.ts.map +1 -1
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/preemptive.d.ts +2 -2
- package/dist/src/preemptive.d.ts.map +1 -1
- package/dist/src/replay/orchestrator.d.ts +2 -2
- package/dist/src/replay/orchestrator.d.ts.map +1 -1
- package/dist/src/resolution/agent-resolver.d.ts +1 -1
- package/dist/src/resolution/agent-resolver.d.ts.map +1 -1
- package/dist/src/resolution/fallback-resolver.d.ts +1 -1
- package/dist/src/resolution/fallback-resolver.d.ts.map +1 -1
- package/dist/src/state/model-health.d.ts +1 -1
- package/dist/src/state/model-health.d.ts.map +1 -1
- package/dist/src/state/store.d.ts +2 -2
- package/dist/src/state/store.d.ts.map +1 -1
- package/dist/src/tools/fallback-status.d.ts +1 -1
- package/dist/src/tools/fallback-status.d.ts.map +1 -1
- package/package.json +4 -3
- package/plugin.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,550 +1,262 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
2
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
-
import { dirname as dirname2, join as join4 } from "path";
|
|
4
3
|
import { homedir as homedir5 } from "os";
|
|
4
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
5
5
|
|
|
6
|
-
// src/config/loader.ts
|
|
7
|
-
import {
|
|
8
|
-
import { basename as basename2, join as join3 } from "path";
|
|
9
|
-
import { homedir as homedir4 } from "os";
|
|
10
|
-
|
|
11
|
-
// src/config/schema.ts
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import { homedir as homedir2 } from "os";
|
|
14
|
-
import { isAbsolute, relative, resolve } from "path";
|
|
6
|
+
// src/config/agent-loader.ts
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
15
8
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
agents: {
|
|
40
|
-
"*": {
|
|
41
|
-
fallbackModels: []
|
|
9
|
+
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
10
|
+
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
11
|
+
function isNothing(subject) {
|
|
12
|
+
return typeof subject === "undefined" || subject === null;
|
|
13
|
+
}
|
|
14
|
+
function isObject(subject) {
|
|
15
|
+
return typeof subject === "object" && subject !== null;
|
|
16
|
+
}
|
|
17
|
+
function toArray(sequence) {
|
|
18
|
+
if (Array.isArray(sequence))
|
|
19
|
+
return sequence;
|
|
20
|
+
else if (isNothing(sequence))
|
|
21
|
+
return [];
|
|
22
|
+
return [sequence];
|
|
23
|
+
}
|
|
24
|
+
function extend(target, source) {
|
|
25
|
+
var index, length, key, sourceKeys;
|
|
26
|
+
if (source) {
|
|
27
|
+
sourceKeys = Object.keys(source);
|
|
28
|
+
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
29
|
+
key = sourceKeys[index];
|
|
30
|
+
target[key] = source[key];
|
|
42
31
|
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
}
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
35
|
+
function repeat(string, count) {
|
|
36
|
+
var result = "", cycle;
|
|
37
|
+
for (cycle = 0;cycle < count; cycle += 1) {
|
|
38
|
+
result += string;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function isNegativeZero(number) {
|
|
43
|
+
return number === 0 && Number.NEGATIVE_INFINITY === 1 / number;
|
|
44
|
+
}
|
|
45
|
+
var isNothing_1 = isNothing;
|
|
46
|
+
var isObject_1 = isObject;
|
|
47
|
+
var toArray_1 = toArray;
|
|
48
|
+
var repeat_1 = repeat;
|
|
49
|
+
var isNegativeZero_1 = isNegativeZero;
|
|
50
|
+
var extend_1 = extend;
|
|
51
|
+
var common = {
|
|
52
|
+
isNothing: isNothing_1,
|
|
53
|
+
isObject: isObject_1,
|
|
54
|
+
toArray: toArray_1,
|
|
55
|
+
repeat: repeat_1,
|
|
56
|
+
isNegativeZero: isNegativeZero_1,
|
|
57
|
+
extend: extend_1
|
|
49
58
|
};
|
|
59
|
+
function formatError(exception, compact) {
|
|
60
|
+
var where = "", message = exception.reason || "(unknown reason)";
|
|
61
|
+
if (!exception.mark)
|
|
62
|
+
return message;
|
|
63
|
+
if (exception.mark.name) {
|
|
64
|
+
where += 'in "' + exception.mark.name + '" ';
|
|
65
|
+
}
|
|
66
|
+
where += "(" + (exception.mark.line + 1) + ":" + (exception.mark.column + 1) + ")";
|
|
67
|
+
if (!compact && exception.mark.snippet) {
|
|
68
|
+
where += `
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
var MODEL_KEY_RE = /^[a-zA-Z0-9_-]{1,100}\/[a-zA-Z0-9._-]{1,100}$/;
|
|
53
|
-
var home = resolve(homedir2());
|
|
54
|
-
function formatPath(path) {
|
|
55
|
-
return path.map((segment) => String(segment)).join(".");
|
|
56
|
-
}
|
|
57
|
-
function normalizeLogPath(path) {
|
|
58
|
-
if (path.startsWith("~/")) {
|
|
59
|
-
return resolve(home, path.slice(2));
|
|
70
|
+
` + exception.mark.snippet;
|
|
60
71
|
}
|
|
61
|
-
return
|
|
72
|
+
return message + " " + where;
|
|
62
73
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
function YAMLException$1(reason, mark) {
|
|
75
|
+
Error.call(this);
|
|
76
|
+
this.name = "YAMLException";
|
|
77
|
+
this.reason = reason;
|
|
78
|
+
this.mark = mark;
|
|
79
|
+
this.message = formatError(this, false);
|
|
80
|
+
if (Error.captureStackTrace) {
|
|
81
|
+
Error.captureStackTrace(this, this.constructor);
|
|
82
|
+
} else {
|
|
83
|
+
this.stack = new Error().stack || "";
|
|
84
|
+
}
|
|
66
85
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
defaults: fallbackDefaults.optional(),
|
|
81
|
-
agents: z.record(z.string(), agentConfig).optional(),
|
|
82
|
-
patterns: z.array(z.string()).optional(),
|
|
83
|
-
logging: z.boolean().optional(),
|
|
84
|
-
logLevel: z.enum(["debug", "info"]).optional(),
|
|
85
|
-
logPath: logPathSchema.optional(),
|
|
86
|
-
agentDirs: z.array(z.string()).optional()
|
|
87
|
-
}).strict();
|
|
88
|
-
function parseConfig(raw) {
|
|
89
|
-
const warnings = [];
|
|
90
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
91
|
-
warnings.push("Config warning at root: expected object — using default");
|
|
92
|
-
return { config: {}, warnings };
|
|
86
|
+
YAMLException$1.prototype = Object.create(Error.prototype);
|
|
87
|
+
YAMLException$1.prototype.constructor = YAMLException$1;
|
|
88
|
+
YAMLException$1.prototype.toString = function toString(compact) {
|
|
89
|
+
return this.name + ": " + formatError(this, compact);
|
|
90
|
+
};
|
|
91
|
+
var exception = YAMLException$1;
|
|
92
|
+
function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
|
|
93
|
+
var head = "";
|
|
94
|
+
var tail = "";
|
|
95
|
+
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
|
|
96
|
+
if (position - lineStart > maxHalfLength) {
|
|
97
|
+
head = " ... ";
|
|
98
|
+
lineStart = position - maxHalfLength + head.length;
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"defaults",
|
|
98
|
-
"agents",
|
|
99
|
-
"patterns",
|
|
100
|
-
"logging",
|
|
101
|
-
"logLevel",
|
|
102
|
-
"logPath",
|
|
103
|
-
"agentDirs"
|
|
104
|
-
]);
|
|
105
|
-
for (const key of Object.keys(obj)) {
|
|
106
|
-
if (!allowed.has(key)) {
|
|
107
|
-
warnings.push(`Config warning at ${key}: unknown field — using default`);
|
|
108
|
-
}
|
|
100
|
+
if (lineEnd - position > maxHalfLength) {
|
|
101
|
+
tail = " ...";
|
|
102
|
+
lineEnd = position + maxHalfLength - tail.length;
|
|
109
103
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
return {
|
|
105
|
+
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
106
|
+
pos: position - lineStart + head.length
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function padStart(string, max) {
|
|
110
|
+
return common.repeat(" ", max - string.length) + string;
|
|
111
|
+
}
|
|
112
|
+
function makeSnippet(mark, options) {
|
|
113
|
+
options = Object.create(options || null);
|
|
114
|
+
if (!mark.buffer)
|
|
115
|
+
return null;
|
|
116
|
+
if (!options.maxLength)
|
|
117
|
+
options.maxLength = 79;
|
|
118
|
+
if (typeof options.indent !== "number")
|
|
119
|
+
options.indent = 1;
|
|
120
|
+
if (typeof options.linesBefore !== "number")
|
|
121
|
+
options.linesBefore = 3;
|
|
122
|
+
if (typeof options.linesAfter !== "number")
|
|
123
|
+
options.linesAfter = 2;
|
|
124
|
+
var re = /\r?\n|\r|\0/g;
|
|
125
|
+
var lineStarts = [0];
|
|
126
|
+
var lineEnds = [];
|
|
127
|
+
var match;
|
|
128
|
+
var foundLineNo = -1;
|
|
129
|
+
while (match = re.exec(mark.buffer)) {
|
|
130
|
+
lineEnds.push(match.index);
|
|
131
|
+
lineStarts.push(match.index + match[0].length);
|
|
132
|
+
if (mark.position <= match.index && foundLineNo < 0) {
|
|
133
|
+
foundLineNo = lineStarts.length - 2;
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
for (const issue of fallbackOnResult.error.issues) {
|
|
131
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
132
|
-
warnings.push(`Config warning at defaults.fallbackOn${suffix}: ${issue.message} — using default`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const cooldownResult = fallbackDefaults.shape.cooldownMs.safeParse(defaultsObj.cooldownMs);
|
|
137
|
-
if (defaultsObj.cooldownMs !== undefined) {
|
|
138
|
-
if (cooldownResult.success && cooldownResult.data !== undefined) {
|
|
139
|
-
parsedDefaults.cooldownMs = cooldownResult.data;
|
|
140
|
-
} else if (!cooldownResult.success) {
|
|
141
|
-
for (const issue of cooldownResult.error.issues) {
|
|
142
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
143
|
-
warnings.push(`Config warning at defaults.cooldownMs${suffix}: ${issue.message} — using default`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const retryResult = fallbackDefaults.shape.retryOriginalAfterMs.safeParse(defaultsObj.retryOriginalAfterMs);
|
|
148
|
-
if (defaultsObj.retryOriginalAfterMs !== undefined) {
|
|
149
|
-
if (retryResult.success && retryResult.data !== undefined) {
|
|
150
|
-
parsedDefaults.retryOriginalAfterMs = retryResult.data;
|
|
151
|
-
} else if (!retryResult.success) {
|
|
152
|
-
for (const issue of retryResult.error.issues) {
|
|
153
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
154
|
-
warnings.push(`Config warning at defaults.retryOriginalAfterMs${suffix}: ${issue.message} — using default`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const depthResult = fallbackDefaults.shape.maxFallbackDepth.safeParse(defaultsObj.maxFallbackDepth);
|
|
159
|
-
if (defaultsObj.maxFallbackDepth !== undefined) {
|
|
160
|
-
if (depthResult.success && depthResult.data !== undefined) {
|
|
161
|
-
parsedDefaults.maxFallbackDepth = depthResult.data;
|
|
162
|
-
} else if (!depthResult.success) {
|
|
163
|
-
for (const issue of depthResult.error.issues) {
|
|
164
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
165
|
-
warnings.push(`Config warning at defaults.maxFallbackDepth${suffix}: ${issue.message} — using default`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const key of Object.keys(defaultsObj)) {
|
|
170
|
-
if (!Object.hasOwn(fallbackDefaults.shape, key)) {
|
|
171
|
-
warnings.push(`Config warning at defaults.${key}: unknown field — using default`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (Object.keys(parsedDefaults).length > 0) {
|
|
175
|
-
config.defaults = parsedDefaults;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (obj.agents !== undefined) {
|
|
180
|
-
if (!obj.agents || typeof obj.agents !== "object" || Array.isArray(obj.agents)) {
|
|
181
|
-
warnings.push("Config warning at agents: expected object — using default");
|
|
182
|
-
} else {
|
|
183
|
-
const parsedAgents = {};
|
|
184
|
-
for (const [agentName, agentValue] of Object.entries(obj.agents)) {
|
|
185
|
-
const agentResult = agentConfig.safeParse(agentValue);
|
|
186
|
-
if (agentResult.success) {
|
|
187
|
-
parsedAgents[agentName] = agentResult.data;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
for (const issue of agentResult.error.issues) {
|
|
191
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
192
|
-
warnings.push(`Config warning at agents.${agentName}${suffix}: ${issue.message} — using default`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
config.agents = parsedAgents;
|
|
196
|
-
}
|
|
136
|
+
if (foundLineNo < 0)
|
|
137
|
+
foundLineNo = lineStarts.length - 1;
|
|
138
|
+
var result = "", i, line;
|
|
139
|
+
var lineNoLength = Math.min(mark.line + options.linesAfter, lineEnds.length).toString().length;
|
|
140
|
+
var maxLineLength = options.maxLength - (options.indent + lineNoLength + 3);
|
|
141
|
+
for (i = 1;i <= options.linesBefore; i++) {
|
|
142
|
+
if (foundLineNo - i < 0)
|
|
143
|
+
break;
|
|
144
|
+
line = getLine(mark.buffer, lineStarts[foundLineNo - i], lineEnds[foundLineNo - i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]), maxLineLength);
|
|
145
|
+
result = common.repeat(" ", options.indent) + padStart((mark.line - i + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
146
|
+
` + result;
|
|
197
147
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
148
|
+
line = getLine(mark.buffer, lineStarts[foundLineNo], lineEnds[foundLineNo], mark.position, maxLineLength);
|
|
149
|
+
result += common.repeat(" ", options.indent) + padStart((mark.line + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
150
|
+
`;
|
|
151
|
+
result += common.repeat("-", options.indent + lineNoLength + 3 + line.pos) + "^" + `
|
|
152
|
+
`;
|
|
153
|
+
for (i = 1;i <= options.linesAfter; i++) {
|
|
154
|
+
if (foundLineNo + i >= lineEnds.length)
|
|
155
|
+
break;
|
|
156
|
+
line = getLine(mark.buffer, lineStarts[foundLineNo + i], lineEnds[foundLineNo + i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]), maxLineLength);
|
|
157
|
+
result += common.repeat(" ", options.indent) + padStart((mark.line + i + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
158
|
+
`;
|
|
208
159
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
160
|
+
return result.replace(/\n$/, "");
|
|
161
|
+
}
|
|
162
|
+
var snippet = makeSnippet;
|
|
163
|
+
var TYPE_CONSTRUCTOR_OPTIONS = [
|
|
164
|
+
"kind",
|
|
165
|
+
"multi",
|
|
166
|
+
"resolve",
|
|
167
|
+
"construct",
|
|
168
|
+
"instanceOf",
|
|
169
|
+
"predicate",
|
|
170
|
+
"represent",
|
|
171
|
+
"representName",
|
|
172
|
+
"defaultStyle",
|
|
173
|
+
"styleAliases"
|
|
174
|
+
];
|
|
175
|
+
var YAML_NODE_KINDS = [
|
|
176
|
+
"scalar",
|
|
177
|
+
"sequence",
|
|
178
|
+
"mapping"
|
|
179
|
+
];
|
|
180
|
+
function compileStyleAliases(map) {
|
|
181
|
+
var result = {};
|
|
182
|
+
if (map !== null) {
|
|
183
|
+
Object.keys(map).forEach(function(style) {
|
|
184
|
+
map[style].forEach(function(alias) {
|
|
185
|
+
result[String(alias)] = style;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
216
188
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function Type$1(tag, options) {
|
|
192
|
+
options = options || {};
|
|
193
|
+
Object.keys(options).forEach(function(name) {
|
|
194
|
+
if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) {
|
|
195
|
+
throw new exception('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.');
|
|
223
196
|
}
|
|
197
|
+
});
|
|
198
|
+
this.options = options;
|
|
199
|
+
this.tag = tag;
|
|
200
|
+
this.kind = options["kind"] || null;
|
|
201
|
+
this.resolve = options["resolve"] || function() {
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
this.construct = options["construct"] || function(data) {
|
|
205
|
+
return data;
|
|
206
|
+
};
|
|
207
|
+
this.instanceOf = options["instanceOf"] || null;
|
|
208
|
+
this.predicate = options["predicate"] || null;
|
|
209
|
+
this.represent = options["represent"] || null;
|
|
210
|
+
this.representName = options["representName"] || null;
|
|
211
|
+
this.defaultStyle = options["defaultStyle"] || null;
|
|
212
|
+
this.multi = options["multi"] || false;
|
|
213
|
+
this.styleAliases = compileStyleAliases(options["styleAliases"] || null);
|
|
214
|
+
if (YAML_NODE_KINDS.indexOf(this.kind) === -1) {
|
|
215
|
+
throw new exception('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.');
|
|
224
216
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
217
|
+
}
|
|
218
|
+
var type = Type$1;
|
|
219
|
+
function compileList(schema, name) {
|
|
220
|
+
var result = [];
|
|
221
|
+
schema[name].forEach(function(currentType) {
|
|
222
|
+
var newIndex = result.length;
|
|
223
|
+
result.forEach(function(previousType, previousIndex) {
|
|
224
|
+
if (previousType.tag === currentType.tag && previousType.kind === currentType.kind && previousType.multi === currentType.multi) {
|
|
225
|
+
newIndex = previousIndex;
|
|
233
226
|
}
|
|
227
|
+
});
|
|
228
|
+
result[newIndex] = currentType;
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
function compileMap() {
|
|
233
|
+
var result = {
|
|
234
|
+
scalar: {},
|
|
235
|
+
sequence: {},
|
|
236
|
+
mapping: {},
|
|
237
|
+
fallback: {},
|
|
238
|
+
multi: {
|
|
239
|
+
scalar: [],
|
|
240
|
+
sequence: [],
|
|
241
|
+
mapping: [],
|
|
242
|
+
fallback: []
|
|
234
243
|
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
}, index, length;
|
|
245
|
+
function collectType(type2) {
|
|
246
|
+
if (type2.multi) {
|
|
247
|
+
result.multi[type2.kind].push(type2);
|
|
248
|
+
result.multi["fallback"].push(type2);
|
|
240
249
|
} else {
|
|
241
|
-
|
|
242
|
-
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
243
|
-
warnings.push(`Config warning at agentDirs${suffix}: ${issue.message} — using default`);
|
|
244
|
-
}
|
|
250
|
+
result[type2.kind][type2.tag] = result["fallback"][type2.tag] = type2;
|
|
245
251
|
}
|
|
246
252
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
function mergeWithDefaults(raw) {
|
|
250
|
-
const def = DEFAULT_CONFIG;
|
|
251
|
-
const logPath = raw.logPath ? normalizeLogPath(raw.logPath) : def.logPath;
|
|
252
|
-
return {
|
|
253
|
-
enabled: raw.enabled ?? def.enabled,
|
|
254
|
-
defaults: {
|
|
255
|
-
fallbackOn: raw.defaults?.fallbackOn ?? def.defaults.fallbackOn,
|
|
256
|
-
cooldownMs: raw.defaults?.cooldownMs ?? def.defaults.cooldownMs,
|
|
257
|
-
retryOriginalAfterMs: raw.defaults?.retryOriginalAfterMs ?? def.defaults.retryOriginalAfterMs,
|
|
258
|
-
maxFallbackDepth: raw.defaults?.maxFallbackDepth ?? def.defaults.maxFallbackDepth
|
|
259
|
-
},
|
|
260
|
-
agents: raw.agents ?? def.agents,
|
|
261
|
-
patterns: raw.patterns ?? def.patterns,
|
|
262
|
-
logging: raw.logging ?? def.logging,
|
|
263
|
-
logLevel: raw.logLevel ?? def.logLevel,
|
|
264
|
-
logPath,
|
|
265
|
-
agentDirs: raw.agentDirs ?? def.agentDirs
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// src/config/migrate.ts
|
|
270
|
-
function isOldFormat(raw) {
|
|
271
|
-
if (typeof raw !== "object" || raw === null)
|
|
272
|
-
return false;
|
|
273
|
-
return "fallbackModel" in raw && typeof raw.fallbackModel === "string";
|
|
274
|
-
}
|
|
275
|
-
function migrateOldConfig(old) {
|
|
276
|
-
const migrated = {};
|
|
277
|
-
if (typeof old.enabled === "boolean")
|
|
278
|
-
migrated.enabled = old.enabled;
|
|
279
|
-
if (typeof old.logging === "boolean")
|
|
280
|
-
migrated.logging = old.logging;
|
|
281
|
-
if (Array.isArray(old.patterns))
|
|
282
|
-
migrated.patterns = old.patterns;
|
|
283
|
-
if (old.fallbackModel) {
|
|
284
|
-
migrated.agents = {
|
|
285
|
-
"*": { fallbackModels: [old.fallbackModel] }
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
if (typeof old.cooldownMs === "number") {
|
|
289
|
-
migrated.defaults = { cooldownMs: old.cooldownMs };
|
|
253
|
+
for (index = 0, length = arguments.length;index < length; index += 1) {
|
|
254
|
+
arguments[index].forEach(collectType);
|
|
290
255
|
}
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// src/config/agent-loader.ts
|
|
295
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
296
|
-
|
|
297
|
-
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
298
|
-
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
299
|
-
function isNothing(subject) {
|
|
300
|
-
return typeof subject === "undefined" || subject === null;
|
|
301
|
-
}
|
|
302
|
-
function isObject(subject) {
|
|
303
|
-
return typeof subject === "object" && subject !== null;
|
|
256
|
+
return result;
|
|
304
257
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
return sequence;
|
|
308
|
-
else if (isNothing(sequence))
|
|
309
|
-
return [];
|
|
310
|
-
return [sequence];
|
|
311
|
-
}
|
|
312
|
-
function extend(target, source) {
|
|
313
|
-
var index, length, key, sourceKeys;
|
|
314
|
-
if (source) {
|
|
315
|
-
sourceKeys = Object.keys(source);
|
|
316
|
-
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
317
|
-
key = sourceKeys[index];
|
|
318
|
-
target[key] = source[key];
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return target;
|
|
322
|
-
}
|
|
323
|
-
function repeat(string, count) {
|
|
324
|
-
var result = "", cycle;
|
|
325
|
-
for (cycle = 0;cycle < count; cycle += 1) {
|
|
326
|
-
result += string;
|
|
327
|
-
}
|
|
328
|
-
return result;
|
|
329
|
-
}
|
|
330
|
-
function isNegativeZero(number) {
|
|
331
|
-
return number === 0 && Number.NEGATIVE_INFINITY === 1 / number;
|
|
332
|
-
}
|
|
333
|
-
var isNothing_1 = isNothing;
|
|
334
|
-
var isObject_1 = isObject;
|
|
335
|
-
var toArray_1 = toArray;
|
|
336
|
-
var repeat_1 = repeat;
|
|
337
|
-
var isNegativeZero_1 = isNegativeZero;
|
|
338
|
-
var extend_1 = extend;
|
|
339
|
-
var common = {
|
|
340
|
-
isNothing: isNothing_1,
|
|
341
|
-
isObject: isObject_1,
|
|
342
|
-
toArray: toArray_1,
|
|
343
|
-
repeat: repeat_1,
|
|
344
|
-
isNegativeZero: isNegativeZero_1,
|
|
345
|
-
extend: extend_1
|
|
346
|
-
};
|
|
347
|
-
function formatError(exception, compact) {
|
|
348
|
-
var where = "", message = exception.reason || "(unknown reason)";
|
|
349
|
-
if (!exception.mark)
|
|
350
|
-
return message;
|
|
351
|
-
if (exception.mark.name) {
|
|
352
|
-
where += 'in "' + exception.mark.name + '" ';
|
|
353
|
-
}
|
|
354
|
-
where += "(" + (exception.mark.line + 1) + ":" + (exception.mark.column + 1) + ")";
|
|
355
|
-
if (!compact && exception.mark.snippet) {
|
|
356
|
-
where += `
|
|
357
|
-
|
|
358
|
-
` + exception.mark.snippet;
|
|
359
|
-
}
|
|
360
|
-
return message + " " + where;
|
|
361
|
-
}
|
|
362
|
-
function YAMLException$1(reason, mark) {
|
|
363
|
-
Error.call(this);
|
|
364
|
-
this.name = "YAMLException";
|
|
365
|
-
this.reason = reason;
|
|
366
|
-
this.mark = mark;
|
|
367
|
-
this.message = formatError(this, false);
|
|
368
|
-
if (Error.captureStackTrace) {
|
|
369
|
-
Error.captureStackTrace(this, this.constructor);
|
|
370
|
-
} else {
|
|
371
|
-
this.stack = new Error().stack || "";
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
YAMLException$1.prototype = Object.create(Error.prototype);
|
|
375
|
-
YAMLException$1.prototype.constructor = YAMLException$1;
|
|
376
|
-
YAMLException$1.prototype.toString = function toString(compact) {
|
|
377
|
-
return this.name + ": " + formatError(this, compact);
|
|
378
|
-
};
|
|
379
|
-
var exception = YAMLException$1;
|
|
380
|
-
function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
|
|
381
|
-
var head = "";
|
|
382
|
-
var tail = "";
|
|
383
|
-
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
|
|
384
|
-
if (position - lineStart > maxHalfLength) {
|
|
385
|
-
head = " ... ";
|
|
386
|
-
lineStart = position - maxHalfLength + head.length;
|
|
387
|
-
}
|
|
388
|
-
if (lineEnd - position > maxHalfLength) {
|
|
389
|
-
tail = " ...";
|
|
390
|
-
lineEnd = position + maxHalfLength - tail.length;
|
|
391
|
-
}
|
|
392
|
-
return {
|
|
393
|
-
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
394
|
-
pos: position - lineStart + head.length
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
function padStart(string, max) {
|
|
398
|
-
return common.repeat(" ", max - string.length) + string;
|
|
399
|
-
}
|
|
400
|
-
function makeSnippet(mark, options) {
|
|
401
|
-
options = Object.create(options || null);
|
|
402
|
-
if (!mark.buffer)
|
|
403
|
-
return null;
|
|
404
|
-
if (!options.maxLength)
|
|
405
|
-
options.maxLength = 79;
|
|
406
|
-
if (typeof options.indent !== "number")
|
|
407
|
-
options.indent = 1;
|
|
408
|
-
if (typeof options.linesBefore !== "number")
|
|
409
|
-
options.linesBefore = 3;
|
|
410
|
-
if (typeof options.linesAfter !== "number")
|
|
411
|
-
options.linesAfter = 2;
|
|
412
|
-
var re = /\r?\n|\r|\0/g;
|
|
413
|
-
var lineStarts = [0];
|
|
414
|
-
var lineEnds = [];
|
|
415
|
-
var match;
|
|
416
|
-
var foundLineNo = -1;
|
|
417
|
-
while (match = re.exec(mark.buffer)) {
|
|
418
|
-
lineEnds.push(match.index);
|
|
419
|
-
lineStarts.push(match.index + match[0].length);
|
|
420
|
-
if (mark.position <= match.index && foundLineNo < 0) {
|
|
421
|
-
foundLineNo = lineStarts.length - 2;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (foundLineNo < 0)
|
|
425
|
-
foundLineNo = lineStarts.length - 1;
|
|
426
|
-
var result = "", i, line;
|
|
427
|
-
var lineNoLength = Math.min(mark.line + options.linesAfter, lineEnds.length).toString().length;
|
|
428
|
-
var maxLineLength = options.maxLength - (options.indent + lineNoLength + 3);
|
|
429
|
-
for (i = 1;i <= options.linesBefore; i++) {
|
|
430
|
-
if (foundLineNo - i < 0)
|
|
431
|
-
break;
|
|
432
|
-
line = getLine(mark.buffer, lineStarts[foundLineNo - i], lineEnds[foundLineNo - i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]), maxLineLength);
|
|
433
|
-
result = common.repeat(" ", options.indent) + padStart((mark.line - i + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
434
|
-
` + result;
|
|
435
|
-
}
|
|
436
|
-
line = getLine(mark.buffer, lineStarts[foundLineNo], lineEnds[foundLineNo], mark.position, maxLineLength);
|
|
437
|
-
result += common.repeat(" ", options.indent) + padStart((mark.line + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
438
|
-
`;
|
|
439
|
-
result += common.repeat("-", options.indent + lineNoLength + 3 + line.pos) + "^" + `
|
|
440
|
-
`;
|
|
441
|
-
for (i = 1;i <= options.linesAfter; i++) {
|
|
442
|
-
if (foundLineNo + i >= lineEnds.length)
|
|
443
|
-
break;
|
|
444
|
-
line = getLine(mark.buffer, lineStarts[foundLineNo + i], lineEnds[foundLineNo + i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]), maxLineLength);
|
|
445
|
-
result += common.repeat(" ", options.indent) + padStart((mark.line + i + 1).toString(), lineNoLength) + " | " + line.str + `
|
|
446
|
-
`;
|
|
447
|
-
}
|
|
448
|
-
return result.replace(/\n$/, "");
|
|
449
|
-
}
|
|
450
|
-
var snippet = makeSnippet;
|
|
451
|
-
var TYPE_CONSTRUCTOR_OPTIONS = [
|
|
452
|
-
"kind",
|
|
453
|
-
"multi",
|
|
454
|
-
"resolve",
|
|
455
|
-
"construct",
|
|
456
|
-
"instanceOf",
|
|
457
|
-
"predicate",
|
|
458
|
-
"represent",
|
|
459
|
-
"representName",
|
|
460
|
-
"defaultStyle",
|
|
461
|
-
"styleAliases"
|
|
462
|
-
];
|
|
463
|
-
var YAML_NODE_KINDS = [
|
|
464
|
-
"scalar",
|
|
465
|
-
"sequence",
|
|
466
|
-
"mapping"
|
|
467
|
-
];
|
|
468
|
-
function compileStyleAliases(map) {
|
|
469
|
-
var result = {};
|
|
470
|
-
if (map !== null) {
|
|
471
|
-
Object.keys(map).forEach(function(style) {
|
|
472
|
-
map[style].forEach(function(alias) {
|
|
473
|
-
result[String(alias)] = style;
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
return result;
|
|
478
|
-
}
|
|
479
|
-
function Type$1(tag, options) {
|
|
480
|
-
options = options || {};
|
|
481
|
-
Object.keys(options).forEach(function(name) {
|
|
482
|
-
if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) {
|
|
483
|
-
throw new exception('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.');
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
this.options = options;
|
|
487
|
-
this.tag = tag;
|
|
488
|
-
this.kind = options["kind"] || null;
|
|
489
|
-
this.resolve = options["resolve"] || function() {
|
|
490
|
-
return true;
|
|
491
|
-
};
|
|
492
|
-
this.construct = options["construct"] || function(data) {
|
|
493
|
-
return data;
|
|
494
|
-
};
|
|
495
|
-
this.instanceOf = options["instanceOf"] || null;
|
|
496
|
-
this.predicate = options["predicate"] || null;
|
|
497
|
-
this.represent = options["represent"] || null;
|
|
498
|
-
this.representName = options["representName"] || null;
|
|
499
|
-
this.defaultStyle = options["defaultStyle"] || null;
|
|
500
|
-
this.multi = options["multi"] || false;
|
|
501
|
-
this.styleAliases = compileStyleAliases(options["styleAliases"] || null);
|
|
502
|
-
if (YAML_NODE_KINDS.indexOf(this.kind) === -1) {
|
|
503
|
-
throw new exception('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.');
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
var type = Type$1;
|
|
507
|
-
function compileList(schema, name) {
|
|
508
|
-
var result = [];
|
|
509
|
-
schema[name].forEach(function(currentType) {
|
|
510
|
-
var newIndex = result.length;
|
|
511
|
-
result.forEach(function(previousType, previousIndex) {
|
|
512
|
-
if (previousType.tag === currentType.tag && previousType.kind === currentType.kind && previousType.multi === currentType.multi) {
|
|
513
|
-
newIndex = previousIndex;
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
result[newIndex] = currentType;
|
|
517
|
-
});
|
|
518
|
-
return result;
|
|
519
|
-
}
|
|
520
|
-
function compileMap() {
|
|
521
|
-
var result = {
|
|
522
|
-
scalar: {},
|
|
523
|
-
sequence: {},
|
|
524
|
-
mapping: {},
|
|
525
|
-
fallback: {},
|
|
526
|
-
multi: {
|
|
527
|
-
scalar: [],
|
|
528
|
-
sequence: [],
|
|
529
|
-
mapping: [],
|
|
530
|
-
fallback: []
|
|
531
|
-
}
|
|
532
|
-
}, index, length;
|
|
533
|
-
function collectType(type2) {
|
|
534
|
-
if (type2.multi) {
|
|
535
|
-
result.multi[type2.kind].push(type2);
|
|
536
|
-
result.multi["fallback"].push(type2);
|
|
537
|
-
} else {
|
|
538
|
-
result[type2.kind][type2.tag] = result["fallback"][type2.tag] = type2;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
for (index = 0, length = arguments.length;index < length; index += 1) {
|
|
542
|
-
arguments[index].forEach(collectType);
|
|
543
|
-
}
|
|
544
|
-
return result;
|
|
545
|
-
}
|
|
546
|
-
function Schema$1(definition) {
|
|
547
|
-
return this.extend(definition);
|
|
258
|
+
function Schema$1(definition) {
|
|
259
|
+
return this.extend(definition);
|
|
548
260
|
}
|
|
549
261
|
Schema$1.prototype.extend = function extend2(definition) {
|
|
550
262
|
var implicit = [];
|
|
@@ -2980,24 +2692,24 @@ var jsYaml = {
|
|
|
2980
2692
|
};
|
|
2981
2693
|
|
|
2982
2694
|
// src/config/agent-loader.ts
|
|
2983
|
-
import { homedir
|
|
2984
|
-
import { basename, extname, isAbsolute
|
|
2985
|
-
var
|
|
2695
|
+
import { homedir } from "os";
|
|
2696
|
+
import { basename, extname, isAbsolute, join, relative, resolve } from "path";
|
|
2697
|
+
var MODEL_KEY_RE = /^[a-zA-Z0-9_-]{1,100}\/[a-zA-Z0-9._-]{1,100}$/;
|
|
2986
2698
|
function isPathInside(baseDir, targetPath) {
|
|
2987
|
-
const rel =
|
|
2988
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2699
|
+
const rel = relative(baseDir, targetPath);
|
|
2700
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
2989
2701
|
}
|
|
2990
|
-
function toRelativeAgentPath(absPath, projectDirectory, homeDir =
|
|
2991
|
-
const resolvedAbs =
|
|
2992
|
-
const configBase =
|
|
2993
|
-
const projectBase =
|
|
2702
|
+
function toRelativeAgentPath(absPath, projectDirectory, homeDir = homedir()) {
|
|
2703
|
+
const resolvedAbs = resolve(absPath);
|
|
2704
|
+
const configBase = resolve(join(homeDir, ".config", "opencode"));
|
|
2705
|
+
const projectBase = resolve(projectDirectory);
|
|
2994
2706
|
if (isPathInside(configBase, resolvedAbs)) {
|
|
2995
|
-
const rel =
|
|
2707
|
+
const rel = relative(configBase, resolvedAbs);
|
|
2996
2708
|
if (rel)
|
|
2997
2709
|
return rel;
|
|
2998
2710
|
}
|
|
2999
2711
|
if (isPathInside(projectBase, resolvedAbs)) {
|
|
3000
|
-
const rel =
|
|
2712
|
+
const rel = relative(projectBase, resolvedAbs);
|
|
3001
2713
|
if (rel)
|
|
3002
2714
|
return rel;
|
|
3003
2715
|
}
|
|
@@ -3011,7 +2723,7 @@ function stemName(filePath) {
|
|
|
3011
2723
|
function collectFiles(dir, recursive) {
|
|
3012
2724
|
if (!existsSync(dir))
|
|
3013
2725
|
return [];
|
|
3014
|
-
const baseDir =
|
|
2726
|
+
const baseDir = resolve(dir);
|
|
3015
2727
|
let baseRealPath = baseDir;
|
|
3016
2728
|
try {
|
|
3017
2729
|
baseRealPath = realpathSync(baseDir);
|
|
@@ -3026,7 +2738,7 @@ function collectFiles(dir, recursive) {
|
|
|
3026
2738
|
entries = readdirSync(baseDir);
|
|
3027
2739
|
}
|
|
3028
2740
|
return entries.filter((e) => e.endsWith(".md") || e.endsWith(".json")).map((e) => {
|
|
3029
|
-
const candidatePath =
|
|
2741
|
+
const candidatePath = resolve(join(baseDir, e));
|
|
3030
2742
|
if (!isPathInside(baseDir, candidatePath))
|
|
3031
2743
|
return null;
|
|
3032
2744
|
try {
|
|
@@ -3078,7 +2790,7 @@ function parseAgentFile(filePath) {
|
|
|
3078
2790
|
return null;
|
|
3079
2791
|
const validModels = [];
|
|
3080
2792
|
for (const m of models) {
|
|
3081
|
-
if (typeof m !== "string" || !
|
|
2793
|
+
if (typeof m !== "string" || !MODEL_KEY_RE.test(m)) {
|
|
3082
2794
|
console.warn(`[model-fallback] agent-loader: skipping invalid model key ${JSON.stringify(m)} in ${basename(filePath)}`);
|
|
3083
2795
|
continue;
|
|
3084
2796
|
}
|
|
@@ -3093,12 +2805,12 @@ function parseAgentFile(filePath) {
|
|
|
3093
2805
|
return null;
|
|
3094
2806
|
}
|
|
3095
2807
|
}
|
|
3096
|
-
function resolveAgentFile(agentName, projectDirectory, customDirs, homeDir =
|
|
2808
|
+
function resolveAgentFile(agentName, projectDirectory, customDirs, homeDir = homedir()) {
|
|
3097
2809
|
const scanDirs = customDirs && customDirs.length > 0 ? customDirs.map((d) => [d, false]) : [
|
|
3098
|
-
[
|
|
3099
|
-
[
|
|
3100
|
-
[
|
|
3101
|
-
[
|
|
2810
|
+
[join(homeDir, ".config", "opencode", "agents"), false],
|
|
2811
|
+
[join(homeDir, ".config", "opencode", "agent"), true],
|
|
2812
|
+
[join(projectDirectory, ".opencode", "agents"), false],
|
|
2813
|
+
[join(projectDirectory, ".opencode", "agent"), true]
|
|
3102
2814
|
];
|
|
3103
2815
|
const allFiles = [];
|
|
3104
2816
|
for (const [dir, recursive] of scanDirs) {
|
|
@@ -3119,12 +2831,12 @@ function resolveAgentFile(agentName, projectDirectory, customDirs, homeDir = hom
|
|
|
3119
2831
|
}
|
|
3120
2832
|
return null;
|
|
3121
2833
|
}
|
|
3122
|
-
function loadAgentFallbackConfigs(projectDirectory, homeDir =
|
|
2834
|
+
function loadAgentFallbackConfigs(projectDirectory, homeDir = homedir()) {
|
|
3123
2835
|
const scanDirs = [
|
|
3124
|
-
[
|
|
3125
|
-
[
|
|
3126
|
-
[
|
|
3127
|
-
[
|
|
2836
|
+
[join(homeDir, ".config", "opencode", "agents"), false],
|
|
2837
|
+
[join(homeDir, ".config", "opencode", "agent"), true],
|
|
2838
|
+
[join(projectDirectory, ".opencode", "agents"), false],
|
|
2839
|
+
[join(projectDirectory, ".opencode", "agent"), true]
|
|
3128
2840
|
];
|
|
3129
2841
|
const result = {};
|
|
3130
2842
|
for (const [dir, recursive] of scanDirs) {
|
|
@@ -3140,325 +2852,346 @@ function loadAgentFallbackConfigs(projectDirectory, homeDir = homedir3()) {
|
|
|
3140
2852
|
}
|
|
3141
2853
|
|
|
3142
2854
|
// src/config/loader.ts
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
const home2 = homedir4();
|
|
3147
|
-
return [
|
|
3148
|
-
join3(directory, ".opencode", CONFIG_FILENAME),
|
|
3149
|
-
join3(home2, ".config", "opencode", CONFIG_FILENAME),
|
|
3150
|
-
join3(directory, ".opencode", OLD_CONFIG_FILENAME),
|
|
3151
|
-
join3(home2, ".config", "opencode", OLD_CONFIG_FILENAME)
|
|
3152
|
-
];
|
|
3153
|
-
}
|
|
3154
|
-
function loadConfig(directory) {
|
|
3155
|
-
const agentFileConfigs = loadAgentFallbackConfigs(directory);
|
|
3156
|
-
const candidates = candidatePaths(directory);
|
|
3157
|
-
for (const candidate of candidates) {
|
|
3158
|
-
if (!existsSync2(candidate))
|
|
3159
|
-
continue;
|
|
3160
|
-
let raw;
|
|
3161
|
-
try {
|
|
3162
|
-
raw = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
3163
|
-
} catch {
|
|
3164
|
-
return {
|
|
3165
|
-
config: {
|
|
3166
|
-
...DEFAULT_CONFIG,
|
|
3167
|
-
agents: { ...agentFileConfigs, ...DEFAULT_CONFIG.agents }
|
|
3168
|
-
},
|
|
3169
|
-
path: candidate,
|
|
3170
|
-
warnings: [`Failed to parse ${basename2(candidate)}: invalid JSON — using defaults`],
|
|
3171
|
-
migrated: false
|
|
3172
|
-
};
|
|
3173
|
-
}
|
|
3174
|
-
const isOld = isOldFormat(raw);
|
|
3175
|
-
if (isOld) {
|
|
3176
|
-
raw = migrateOldConfig(raw);
|
|
3177
|
-
}
|
|
3178
|
-
const { config: parsed, warnings } = parseConfig(raw);
|
|
3179
|
-
const merged = mergeWithDefaults(parsed);
|
|
3180
|
-
merged.agents = { ...agentFileConfigs, ...merged.agents };
|
|
3181
|
-
return {
|
|
3182
|
-
config: merged,
|
|
3183
|
-
path: candidate,
|
|
3184
|
-
warnings,
|
|
3185
|
-
migrated: isOld
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
return {
|
|
3189
|
-
config: {
|
|
3190
|
-
...DEFAULT_CONFIG,
|
|
3191
|
-
agents: { ...agentFileConfigs, ...DEFAULT_CONFIG.agents }
|
|
3192
|
-
},
|
|
3193
|
-
path: null,
|
|
3194
|
-
warnings: [],
|
|
3195
|
-
migrated: false
|
|
3196
|
-
};
|
|
3197
|
-
}
|
|
2855
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2856
|
+
import { homedir as homedir4 } from "os";
|
|
2857
|
+
import { basename as basename2, join as join3 } from "path";
|
|
3198
2858
|
|
|
3199
|
-
// src/
|
|
3200
|
-
import {
|
|
3201
|
-
import {
|
|
2859
|
+
// src/config/defaults.ts
|
|
2860
|
+
import { homedir as homedir2 } from "os";
|
|
2861
|
+
import { join as join2 } from "path";
|
|
2862
|
+
var DEFAULT_PATTERNS = [
|
|
2863
|
+
"rate limit",
|
|
2864
|
+
"usage limit",
|
|
2865
|
+
"too many requests",
|
|
2866
|
+
"quota exceeded",
|
|
2867
|
+
"overloaded",
|
|
2868
|
+
"capacity exceeded",
|
|
2869
|
+
"credits exhausted",
|
|
2870
|
+
"billing limit",
|
|
2871
|
+
"429"
|
|
2872
|
+
];
|
|
2873
|
+
var DEFAULT_LOG_PATH = join2(homedir2(), ".local/share/opencode/logs/model-fallback.log");
|
|
2874
|
+
var DEFAULT_CONFIG = {
|
|
2875
|
+
enabled: true,
|
|
2876
|
+
defaults: {
|
|
2877
|
+
fallbackOn: ["rate_limit", "quota_exceeded", "5xx", "timeout", "overloaded"],
|
|
2878
|
+
cooldownMs: 300000,
|
|
2879
|
+
retryOriginalAfterMs: 900000,
|
|
2880
|
+
maxFallbackDepth: 3
|
|
2881
|
+
},
|
|
2882
|
+
agents: {
|
|
2883
|
+
"*": {
|
|
2884
|
+
fallbackModels: []
|
|
2885
|
+
}
|
|
2886
|
+
},
|
|
2887
|
+
patterns: DEFAULT_PATTERNS,
|
|
2888
|
+
logging: true,
|
|
2889
|
+
logLevel: "info",
|
|
2890
|
+
logPath: DEFAULT_LOG_PATH,
|
|
2891
|
+
agentDirs: []
|
|
2892
|
+
};
|
|
3202
2893
|
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
...fields
|
|
2894
|
+
// src/config/migrate.ts
|
|
2895
|
+
function isOldFormat(raw) {
|
|
2896
|
+
if (typeof raw !== "object" || raw === null)
|
|
2897
|
+
return false;
|
|
2898
|
+
return "fallbackModel" in raw && typeof raw.fallbackModel === "string";
|
|
2899
|
+
}
|
|
2900
|
+
function migrateOldConfig(old) {
|
|
2901
|
+
const migrated = {};
|
|
2902
|
+
if (typeof old.enabled === "boolean")
|
|
2903
|
+
migrated.enabled = old.enabled;
|
|
2904
|
+
if (typeof old.logging === "boolean")
|
|
2905
|
+
migrated.logging = old.logging;
|
|
2906
|
+
if (Array.isArray(old.patterns))
|
|
2907
|
+
migrated.patterns = old.patterns;
|
|
2908
|
+
if (old.fallbackModel) {
|
|
2909
|
+
migrated.agents = {
|
|
2910
|
+
"*": { fallbackModels: [old.fallbackModel] }
|
|
3221
2911
|
};
|
|
3222
|
-
const shouldWrite = this.enabled && (this.minLevel === "debug" || level !== "debug");
|
|
3223
|
-
if (shouldWrite) {
|
|
3224
|
-
this.writeToFile(entry);
|
|
3225
|
-
}
|
|
3226
|
-
if (level !== "debug") {
|
|
3227
|
-
const message = `[model-fallback] ${event}${Object.keys(fields).length ? " " + JSON.stringify(fields) : ""}`;
|
|
3228
|
-
this.client.app.log({
|
|
3229
|
-
body: { service: "model-fallback", level, message }
|
|
3230
|
-
}).catch(() => {});
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
info(event, fields) {
|
|
3234
|
-
this.log("info", event, fields);
|
|
3235
|
-
}
|
|
3236
|
-
warn(event, fields) {
|
|
3237
|
-
this.log("warn", event, fields);
|
|
3238
|
-
}
|
|
3239
|
-
error(event, fields) {
|
|
3240
|
-
this.log("error", event, fields);
|
|
3241
|
-
}
|
|
3242
|
-
debug(event, fields) {
|
|
3243
|
-
this.log("debug", event, fields);
|
|
3244
2912
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
if (!this.dirCreated) {
|
|
3248
|
-
mkdirSync(dirname(this.logPath), { recursive: true, mode: 448 });
|
|
3249
|
-
this.dirCreated = true;
|
|
3250
|
-
}
|
|
3251
|
-
if (!existsSync3(this.logPath)) {
|
|
3252
|
-
writeFileSync(this.logPath, "", { mode: 384 });
|
|
3253
|
-
}
|
|
3254
|
-
appendFileSync(this.logPath, JSON.stringify(entry) + `
|
|
3255
|
-
`, "utf-8");
|
|
3256
|
-
} catch {}
|
|
2913
|
+
if (typeof old.cooldownMs === "number") {
|
|
2914
|
+
migrated.defaults = { cooldownMs: old.cooldownMs };
|
|
3257
2915
|
}
|
|
2916
|
+
return migrated;
|
|
3258
2917
|
}
|
|
3259
2918
|
|
|
3260
|
-
// src/
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
markRateLimited(modelKey2, cooldownMs, retryOriginalAfterMs) {
|
|
3273
|
-
const now = Date.now();
|
|
3274
|
-
const existing = this.get(modelKey2);
|
|
3275
|
-
const health = {
|
|
3276
|
-
...existing,
|
|
3277
|
-
state: "rate_limited",
|
|
3278
|
-
lastFailure: now,
|
|
3279
|
-
failureCount: existing.failureCount + 1,
|
|
3280
|
-
cooldownExpiresAt: now + cooldownMs,
|
|
3281
|
-
retryOriginalAt: now + retryOriginalAfterMs
|
|
3282
|
-
};
|
|
3283
|
-
this.store.set(modelKey2, health);
|
|
2919
|
+
// src/config/schema.ts
|
|
2920
|
+
import { homedir as homedir3 } from "os";
|
|
2921
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "path";
|
|
2922
|
+
import { z } from "zod";
|
|
2923
|
+
var MODEL_KEY_RE2 = /^[a-zA-Z0-9_-]{1,100}\/[a-zA-Z0-9._-]{1,100}$/;
|
|
2924
|
+
var home = resolve2(homedir3());
|
|
2925
|
+
function formatPath(path) {
|
|
2926
|
+
return path.map((segment) => String(segment)).join(".");
|
|
2927
|
+
}
|
|
2928
|
+
function normalizeLogPath(path) {
|
|
2929
|
+
if (path.startsWith("~/")) {
|
|
2930
|
+
return resolve2(home, path.slice(2));
|
|
3284
2931
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
2932
|
+
return resolve2(path);
|
|
2933
|
+
}
|
|
2934
|
+
function isPathWithinHome(path) {
|
|
2935
|
+
const rel = relative2(home, path);
|
|
2936
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2937
|
+
}
|
|
2938
|
+
var modelKey = z.string().regex(MODEL_KEY_RE2, "Model key must be 'providerID/modelID'");
|
|
2939
|
+
var agentConfig = z.object({
|
|
2940
|
+
fallbackModels: z.array(modelKey).min(1)
|
|
2941
|
+
});
|
|
2942
|
+
var fallbackDefaults = z.object({
|
|
2943
|
+
fallbackOn: z.array(z.enum(["rate_limit", "quota_exceeded", "5xx", "timeout", "overloaded"])).optional(),
|
|
2944
|
+
cooldownMs: z.number().min(1e4).optional(),
|
|
2945
|
+
retryOriginalAfterMs: z.number().min(1e4).optional(),
|
|
2946
|
+
maxFallbackDepth: z.number().int().min(1).max(10).optional()
|
|
2947
|
+
});
|
|
2948
|
+
var logPathSchema = z.string().refine((p) => isPathWithinHome(normalizeLogPath(p)), "logPath must resolve within $HOME");
|
|
2949
|
+
var pluginConfigSchema = z.object({
|
|
2950
|
+
enabled: z.boolean().optional(),
|
|
2951
|
+
defaults: fallbackDefaults.optional(),
|
|
2952
|
+
agents: z.record(z.string(), agentConfig).optional(),
|
|
2953
|
+
patterns: z.array(z.string()).optional(),
|
|
2954
|
+
logging: z.boolean().optional(),
|
|
2955
|
+
logLevel: z.enum(["debug", "info"]).optional(),
|
|
2956
|
+
logPath: logPathSchema.optional(),
|
|
2957
|
+
agentDirs: z.array(z.string()).optional()
|
|
2958
|
+
}).strict();
|
|
2959
|
+
function parseConfig(raw) {
|
|
2960
|
+
const warnings = [];
|
|
2961
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2962
|
+
warnings.push("Config warning at root: expected object — using default");
|
|
2963
|
+
return { config: {}, warnings };
|
|
3288
2964
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
2965
|
+
const obj = raw;
|
|
2966
|
+
const allowed = new Set([
|
|
2967
|
+
"enabled",
|
|
2968
|
+
"defaults",
|
|
2969
|
+
"agents",
|
|
2970
|
+
"patterns",
|
|
2971
|
+
"logging",
|
|
2972
|
+
"logLevel",
|
|
2973
|
+
"logPath",
|
|
2974
|
+
"agentDirs"
|
|
2975
|
+
]);
|
|
2976
|
+
for (const key of Object.keys(obj)) {
|
|
2977
|
+
if (!allowed.has(key)) {
|
|
2978
|
+
warnings.push(`Config warning at ${key}: unknown field — using default`);
|
|
2979
|
+
}
|
|
3296
2980
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
2981
|
+
const config = {};
|
|
2982
|
+
const enabledResult = z.boolean().safeParse(obj.enabled);
|
|
2983
|
+
if (obj.enabled !== undefined) {
|
|
2984
|
+
if (enabledResult.success) {
|
|
2985
|
+
config.enabled = enabledResult.data;
|
|
2986
|
+
} else {
|
|
2987
|
+
warnings.push(`Config warning at enabled: ${enabledResult.error.issues[0].message} — using default`);
|
|
2988
|
+
}
|
|
3299
2989
|
}
|
|
3300
|
-
|
|
3301
|
-
if (
|
|
3302
|
-
|
|
3303
|
-
|
|
2990
|
+
if (obj.defaults !== undefined) {
|
|
2991
|
+
if (!obj.defaults || typeof obj.defaults !== "object" || Array.isArray(obj.defaults)) {
|
|
2992
|
+
warnings.push("Config warning at defaults: expected object — using default");
|
|
2993
|
+
} else {
|
|
2994
|
+
const defaultsObj = obj.defaults;
|
|
2995
|
+
const parsedDefaults = {};
|
|
2996
|
+
const fallbackOnResult = fallbackDefaults.shape.fallbackOn.safeParse(defaultsObj.fallbackOn);
|
|
2997
|
+
if (defaultsObj.fallbackOn !== undefined) {
|
|
2998
|
+
if (fallbackOnResult.success && fallbackOnResult.data !== undefined) {
|
|
2999
|
+
parsedDefaults.fallbackOn = fallbackOnResult.data;
|
|
3000
|
+
} else if (!fallbackOnResult.success) {
|
|
3001
|
+
for (const issue of fallbackOnResult.error.issues) {
|
|
3002
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3003
|
+
warnings.push(`Config warning at defaults.fallbackOn${suffix}: ${issue.message} — using default`);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
const cooldownResult = fallbackDefaults.shape.cooldownMs.safeParse(defaultsObj.cooldownMs);
|
|
3008
|
+
if (defaultsObj.cooldownMs !== undefined) {
|
|
3009
|
+
if (cooldownResult.success && cooldownResult.data !== undefined) {
|
|
3010
|
+
parsedDefaults.cooldownMs = cooldownResult.data;
|
|
3011
|
+
} else if (!cooldownResult.success) {
|
|
3012
|
+
for (const issue of cooldownResult.error.issues) {
|
|
3013
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3014
|
+
warnings.push(`Config warning at defaults.cooldownMs${suffix}: ${issue.message} — using default`);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
const retryResult = fallbackDefaults.shape.retryOriginalAfterMs.safeParse(defaultsObj.retryOriginalAfterMs);
|
|
3019
|
+
if (defaultsObj.retryOriginalAfterMs !== undefined) {
|
|
3020
|
+
if (retryResult.success && retryResult.data !== undefined) {
|
|
3021
|
+
parsedDefaults.retryOriginalAfterMs = retryResult.data;
|
|
3022
|
+
} else if (!retryResult.success) {
|
|
3023
|
+
for (const issue of retryResult.error.issues) {
|
|
3024
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3025
|
+
warnings.push(`Config warning at defaults.retryOriginalAfterMs${suffix}: ${issue.message} — using default`);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
const depthResult = fallbackDefaults.shape.maxFallbackDepth.safeParse(defaultsObj.maxFallbackDepth);
|
|
3030
|
+
if (defaultsObj.maxFallbackDepth !== undefined) {
|
|
3031
|
+
if (depthResult.success && depthResult.data !== undefined) {
|
|
3032
|
+
parsedDefaults.maxFallbackDepth = depthResult.data;
|
|
3033
|
+
} else if (!depthResult.success) {
|
|
3034
|
+
for (const issue of depthResult.error.issues) {
|
|
3035
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3036
|
+
warnings.push(`Config warning at defaults.maxFallbackDepth${suffix}: ${issue.message} — using default`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
for (const key of Object.keys(defaultsObj)) {
|
|
3041
|
+
if (!Object.hasOwn(fallbackDefaults.shape, key)) {
|
|
3042
|
+
warnings.push(`Config warning at defaults.${key}: unknown field — using default`);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
if (Object.keys(parsedDefaults).length > 0) {
|
|
3046
|
+
config.defaults = parsedDefaults;
|
|
3047
|
+
}
|
|
3304
3048
|
}
|
|
3305
3049
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
this.onTransition?.(key, "cooldown", "healthy");
|
|
3050
|
+
if (obj.agents !== undefined) {
|
|
3051
|
+
if (!obj.agents || typeof obj.agents !== "object" || Array.isArray(obj.agents)) {
|
|
3052
|
+
warnings.push("Config warning at agents: expected object — using default");
|
|
3053
|
+
} else {
|
|
3054
|
+
const parsedAgents = {};
|
|
3055
|
+
for (const [agentName, agentValue] of Object.entries(obj.agents)) {
|
|
3056
|
+
const agentResult = agentConfig.safeParse(agentValue);
|
|
3057
|
+
if (agentResult.success) {
|
|
3058
|
+
parsedAgents[agentName] = agentResult.data;
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
for (const issue of agentResult.error.issues) {
|
|
3062
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3063
|
+
warnings.push(`Config warning at agents.${agentName}${suffix}: ${issue.message} — using default`);
|
|
3064
|
+
}
|
|
3322
3065
|
}
|
|
3066
|
+
config.agents = parsedAgents;
|
|
3323
3067
|
}
|
|
3324
3068
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
|
|
3337
|
-
// src/state/session-state.ts
|
|
3338
|
-
class SessionStateStore {
|
|
3339
|
-
store = new Map;
|
|
3340
|
-
get(sessionId) {
|
|
3341
|
-
let state = this.store.get(sessionId);
|
|
3342
|
-
if (!state) {
|
|
3343
|
-
state = this.newState(sessionId);
|
|
3344
|
-
this.store.set(sessionId, state);
|
|
3069
|
+
if (obj.patterns !== undefined) {
|
|
3070
|
+
const patternsResult = z.array(z.string()).safeParse(obj.patterns);
|
|
3071
|
+
if (patternsResult.success) {
|
|
3072
|
+
config.patterns = patternsResult.data;
|
|
3073
|
+
} else {
|
|
3074
|
+
for (const issue of patternsResult.error.issues) {
|
|
3075
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3076
|
+
warnings.push(`Config warning at patterns${suffix}: ${issue.message} — using default`);
|
|
3077
|
+
}
|
|
3345
3078
|
}
|
|
3346
|
-
return state;
|
|
3347
|
-
}
|
|
3348
|
-
acquireLock(sessionId) {
|
|
3349
|
-
const state = this.get(sessionId);
|
|
3350
|
-
if (state.isProcessing)
|
|
3351
|
-
return false;
|
|
3352
|
-
state.isProcessing = true;
|
|
3353
|
-
return true;
|
|
3354
|
-
}
|
|
3355
|
-
releaseLock(sessionId) {
|
|
3356
|
-
const state = this.store.get(sessionId);
|
|
3357
|
-
if (state)
|
|
3358
|
-
state.isProcessing = false;
|
|
3359
|
-
}
|
|
3360
|
-
isInDedupWindow(sessionId, windowMs = 3000) {
|
|
3361
|
-
const state = this.get(sessionId);
|
|
3362
|
-
if (!state.lastFallbackAt)
|
|
3363
|
-
return false;
|
|
3364
|
-
return Date.now() - state.lastFallbackAt < windowMs;
|
|
3365
|
-
}
|
|
3366
|
-
recordFallback(sessionId, fromModel, toModel, reason, agentName) {
|
|
3367
|
-
const state = this.get(sessionId);
|
|
3368
|
-
const event = {
|
|
3369
|
-
at: Date.now(),
|
|
3370
|
-
fromModel,
|
|
3371
|
-
toModel,
|
|
3372
|
-
reason,
|
|
3373
|
-
sessionId,
|
|
3374
|
-
trigger: "reactive",
|
|
3375
|
-
agentName
|
|
3376
|
-
};
|
|
3377
|
-
state.currentModel = toModel;
|
|
3378
|
-
state.fallbackDepth++;
|
|
3379
|
-
state.lastFallbackAt = event.at;
|
|
3380
|
-
state.recoveryNotifiedForModel = null;
|
|
3381
|
-
state.fallbackHistory.push(event);
|
|
3382
|
-
if (agentName)
|
|
3383
|
-
state.agentName = agentName;
|
|
3384
|
-
}
|
|
3385
|
-
recordPreemptiveRedirect(sessionId, fromModel, toModel, agentName) {
|
|
3386
|
-
const state = this.get(sessionId);
|
|
3387
|
-
const event = {
|
|
3388
|
-
at: Date.now(),
|
|
3389
|
-
fromModel,
|
|
3390
|
-
toModel,
|
|
3391
|
-
reason: "rate_limit",
|
|
3392
|
-
sessionId,
|
|
3393
|
-
trigger: "preemptive",
|
|
3394
|
-
agentName
|
|
3395
|
-
};
|
|
3396
|
-
state.currentModel = toModel;
|
|
3397
|
-
state.recoveryNotifiedForModel = null;
|
|
3398
|
-
state.fallbackHistory.push(event);
|
|
3399
|
-
if (agentName)
|
|
3400
|
-
state.agentName = agentName;
|
|
3401
3079
|
}
|
|
3402
|
-
|
|
3403
|
-
const
|
|
3404
|
-
if (
|
|
3405
|
-
|
|
3406
|
-
|
|
3080
|
+
if (obj.logging !== undefined) {
|
|
3081
|
+
const loggingResult = z.boolean().safeParse(obj.logging);
|
|
3082
|
+
if (loggingResult.success) {
|
|
3083
|
+
config.logging = loggingResult.data;
|
|
3084
|
+
} else {
|
|
3085
|
+
warnings.push(`Config warning at logging: ${loggingResult.error.issues[0].message} — using default`);
|
|
3407
3086
|
}
|
|
3408
3087
|
}
|
|
3409
|
-
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
}
|
|
3417
|
-
partialReset(sessionId) {
|
|
3418
|
-
const state = this.store.get(sessionId);
|
|
3419
|
-
if (!state)
|
|
3420
|
-
return;
|
|
3421
|
-
state.fallbackHistory = [];
|
|
3422
|
-
state.lastFallbackAt = null;
|
|
3423
|
-
state.isProcessing = false;
|
|
3088
|
+
if (obj.logLevel !== undefined) {
|
|
3089
|
+
const logLevelResult = z.enum(["debug", "info"]).safeParse(obj.logLevel);
|
|
3090
|
+
if (logLevelResult.success) {
|
|
3091
|
+
config.logLevel = logLevelResult.data;
|
|
3092
|
+
} else {
|
|
3093
|
+
warnings.push(`Config warning at logLevel: ${logLevelResult.error.issues[0].message} — using default`);
|
|
3094
|
+
}
|
|
3424
3095
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3096
|
+
if (obj.logPath !== undefined) {
|
|
3097
|
+
const logPathResult = logPathSchema.safeParse(obj.logPath);
|
|
3098
|
+
if (logPathResult.success) {
|
|
3099
|
+
config.logPath = obj.logPath;
|
|
3100
|
+
} else {
|
|
3101
|
+
for (const issue of logPathResult.error.issues) {
|
|
3102
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3103
|
+
warnings.push(`Config warning at logPath${suffix}: ${issue.message} — using default`);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3427
3106
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3107
|
+
if (obj.agentDirs !== undefined) {
|
|
3108
|
+
const agentDirsResult = z.array(z.string()).safeParse(obj.agentDirs);
|
|
3109
|
+
if (agentDirsResult.success) {
|
|
3110
|
+
config.agentDirs = agentDirsResult.data;
|
|
3111
|
+
} else {
|
|
3112
|
+
for (const issue of agentDirsResult.error.issues) {
|
|
3113
|
+
const suffix = issue.path.length > 0 ? `.${formatPath(issue.path)}` : "";
|
|
3114
|
+
warnings.push(`Config warning at agentDirs${suffix}: ${issue.message} — using default`);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3430
3117
|
}
|
|
3431
|
-
|
|
3118
|
+
return { config, warnings };
|
|
3119
|
+
}
|
|
3120
|
+
function mergeWithDefaults(raw) {
|
|
3121
|
+
const def = DEFAULT_CONFIG;
|
|
3122
|
+
const logPath = raw.logPath ? normalizeLogPath(raw.logPath) : def.logPath;
|
|
3123
|
+
return {
|
|
3124
|
+
enabled: raw.enabled ?? def.enabled,
|
|
3125
|
+
defaults: {
|
|
3126
|
+
fallbackOn: raw.defaults?.fallbackOn ?? def.defaults.fallbackOn,
|
|
3127
|
+
cooldownMs: raw.defaults?.cooldownMs ?? def.defaults.cooldownMs,
|
|
3128
|
+
retryOriginalAfterMs: raw.defaults?.retryOriginalAfterMs ?? def.defaults.retryOriginalAfterMs,
|
|
3129
|
+
maxFallbackDepth: raw.defaults?.maxFallbackDepth ?? def.defaults.maxFallbackDepth
|
|
3130
|
+
},
|
|
3131
|
+
agents: raw.agents ?? def.agents,
|
|
3132
|
+
patterns: raw.patterns ?? def.patterns,
|
|
3133
|
+
logging: raw.logging ?? def.logging,
|
|
3134
|
+
logLevel: raw.logLevel ?? def.logLevel,
|
|
3135
|
+
logPath,
|
|
3136
|
+
agentDirs: raw.agentDirs ?? def.agentDirs
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// src/config/loader.ts
|
|
3141
|
+
var CONFIG_FILENAME = "model-fallback.json";
|
|
3142
|
+
var OLD_CONFIG_FILENAME = "rate-limit-fallback.json";
|
|
3143
|
+
function candidatePaths(directory) {
|
|
3144
|
+
const home2 = homedir4();
|
|
3145
|
+
return [
|
|
3146
|
+
join3(directory, ".opencode", CONFIG_FILENAME),
|
|
3147
|
+
join3(home2, ".config", "opencode", CONFIG_FILENAME),
|
|
3148
|
+
join3(directory, ".opencode", OLD_CONFIG_FILENAME),
|
|
3149
|
+
join3(home2, ".config", "opencode", OLD_CONFIG_FILENAME)
|
|
3150
|
+
];
|
|
3151
|
+
}
|
|
3152
|
+
function loadConfig(directory) {
|
|
3153
|
+
const agentFileConfigs = loadAgentFallbackConfigs(directory);
|
|
3154
|
+
const candidates = candidatePaths(directory);
|
|
3155
|
+
for (const candidate of candidates) {
|
|
3156
|
+
if (!existsSync2(candidate))
|
|
3157
|
+
continue;
|
|
3158
|
+
let raw;
|
|
3159
|
+
try {
|
|
3160
|
+
raw = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
3161
|
+
} catch {
|
|
3162
|
+
return {
|
|
3163
|
+
config: {
|
|
3164
|
+
...DEFAULT_CONFIG,
|
|
3165
|
+
agents: { ...agentFileConfigs, ...DEFAULT_CONFIG.agents }
|
|
3166
|
+
},
|
|
3167
|
+
path: candidate,
|
|
3168
|
+
warnings: [`Failed to parse ${basename2(candidate)}: invalid JSON — using defaults`],
|
|
3169
|
+
migrated: false
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
const isOld = isOldFormat(raw);
|
|
3173
|
+
if (isOld) {
|
|
3174
|
+
raw = migrateOldConfig(raw);
|
|
3175
|
+
}
|
|
3176
|
+
const { config: parsed, warnings } = parseConfig(raw);
|
|
3177
|
+
const merged = mergeWithDefaults(parsed);
|
|
3178
|
+
merged.agents = { ...agentFileConfigs, ...merged.agents };
|
|
3432
3179
|
return {
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
currentModel: null,
|
|
3438
|
-
fallbackDepth: 0,
|
|
3439
|
-
isProcessing: false,
|
|
3440
|
-
lastFallbackAt: null,
|
|
3441
|
-
fallbackHistory: [],
|
|
3442
|
-
recoveryNotifiedForModel: null
|
|
3180
|
+
config: merged,
|
|
3181
|
+
path: candidate,
|
|
3182
|
+
warnings,
|
|
3183
|
+
migrated: isOld
|
|
3443
3184
|
};
|
|
3444
3185
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
onTransition: (modelKey2, from, to) => {
|
|
3455
|
-
logger.info("health.transition", { modelKey: modelKey2, from, to });
|
|
3456
|
-
}
|
|
3457
|
-
});
|
|
3458
|
-
}
|
|
3459
|
-
destroy() {
|
|
3460
|
-
this.health.destroy();
|
|
3461
|
-
}
|
|
3186
|
+
return {
|
|
3187
|
+
config: {
|
|
3188
|
+
...DEFAULT_CONFIG,
|
|
3189
|
+
agents: { ...agentFileConfigs, ...DEFAULT_CONFIG.agents }
|
|
3190
|
+
},
|
|
3191
|
+
path: null,
|
|
3192
|
+
warnings: [],
|
|
3193
|
+
migrated: false
|
|
3194
|
+
};
|
|
3462
3195
|
}
|
|
3463
3196
|
|
|
3464
3197
|
// src/detection/patterns.ts
|
|
@@ -3514,6 +3247,107 @@ function classifyError(message, statusCode) {
|
|
|
3514
3247
|
return "unknown";
|
|
3515
3248
|
}
|
|
3516
3249
|
|
|
3250
|
+
// src/display/notifier.ts
|
|
3251
|
+
async function notifyFallback(client, from, to, reason) {
|
|
3252
|
+
const fromLabel = from ? shortModelName(from) : "current model";
|
|
3253
|
+
const message = `Model fallback: switched from ${fromLabel} to ${shortModelName(to)} (${reason})`;
|
|
3254
|
+
await client.tui.showToast({
|
|
3255
|
+
body: {
|
|
3256
|
+
title: "Model Fallback",
|
|
3257
|
+
message,
|
|
3258
|
+
variant: "warning",
|
|
3259
|
+
duration: 6000
|
|
3260
|
+
}
|
|
3261
|
+
}).catch(() => {});
|
|
3262
|
+
}
|
|
3263
|
+
async function notifyFallbackActive(client, originalModel, currentModel) {
|
|
3264
|
+
const message = `Using ${shortModelName(currentModel)} (fallback from ${shortModelName(originalModel)})`;
|
|
3265
|
+
await client.tui.showToast({
|
|
3266
|
+
body: {
|
|
3267
|
+
title: "Fallback Active",
|
|
3268
|
+
message,
|
|
3269
|
+
variant: "warning",
|
|
3270
|
+
duration: 4000
|
|
3271
|
+
}
|
|
3272
|
+
}).catch(() => {});
|
|
3273
|
+
}
|
|
3274
|
+
async function notifyRecovery(client, originalModel) {
|
|
3275
|
+
const message = `Original model ${shortModelName(originalModel)} is available again`;
|
|
3276
|
+
await client.tui.showToast({
|
|
3277
|
+
body: {
|
|
3278
|
+
title: "Model Recovered",
|
|
3279
|
+
message,
|
|
3280
|
+
variant: "info",
|
|
3281
|
+
duration: 5000
|
|
3282
|
+
}
|
|
3283
|
+
}).catch(() => {});
|
|
3284
|
+
}
|
|
3285
|
+
function shortModelName(key) {
|
|
3286
|
+
const parts = key.split("/");
|
|
3287
|
+
return parts.length > 1 ? parts.slice(1).join("/") : key;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// src/logging/logger.ts
|
|
3291
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
3292
|
+
import { dirname } from "path";
|
|
3293
|
+
|
|
3294
|
+
class Logger {
|
|
3295
|
+
client;
|
|
3296
|
+
logPath;
|
|
3297
|
+
enabled;
|
|
3298
|
+
minLevel;
|
|
3299
|
+
dirCreated = false;
|
|
3300
|
+
constructor(client, logPath, enabled, minLevel = "info") {
|
|
3301
|
+
this.client = client;
|
|
3302
|
+
this.logPath = logPath;
|
|
3303
|
+
this.enabled = enabled;
|
|
3304
|
+
this.minLevel = minLevel;
|
|
3305
|
+
}
|
|
3306
|
+
log(level, event, fields = {}) {
|
|
3307
|
+
const entry = {
|
|
3308
|
+
ts: new Date().toISOString(),
|
|
3309
|
+
level,
|
|
3310
|
+
event,
|
|
3311
|
+
...fields
|
|
3312
|
+
};
|
|
3313
|
+
const shouldWrite = this.enabled && (this.minLevel === "debug" || level !== "debug");
|
|
3314
|
+
if (shouldWrite) {
|
|
3315
|
+
this.writeToFile(entry);
|
|
3316
|
+
}
|
|
3317
|
+
if (level !== "debug") {
|
|
3318
|
+
const message = `[model-fallback] ${event}${Object.keys(fields).length ? " " + JSON.stringify(fields) : ""}`;
|
|
3319
|
+
this.client.app.log({
|
|
3320
|
+
body: { service: "model-fallback", level, message }
|
|
3321
|
+
}).catch(() => {});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
info(event, fields) {
|
|
3325
|
+
this.log("info", event, fields);
|
|
3326
|
+
}
|
|
3327
|
+
warn(event, fields) {
|
|
3328
|
+
this.log("warn", event, fields);
|
|
3329
|
+
}
|
|
3330
|
+
error(event, fields) {
|
|
3331
|
+
this.log("error", event, fields);
|
|
3332
|
+
}
|
|
3333
|
+
debug(event, fields) {
|
|
3334
|
+
this.log("debug", event, fields);
|
|
3335
|
+
}
|
|
3336
|
+
writeToFile(entry) {
|
|
3337
|
+
try {
|
|
3338
|
+
if (!this.dirCreated) {
|
|
3339
|
+
mkdirSync(dirname(this.logPath), { recursive: true, mode: 448 });
|
|
3340
|
+
this.dirCreated = true;
|
|
3341
|
+
}
|
|
3342
|
+
if (!existsSync3(this.logPath)) {
|
|
3343
|
+
writeFileSync(this.logPath, "", { mode: 384 });
|
|
3344
|
+
}
|
|
3345
|
+
appendFileSync(this.logPath, JSON.stringify(entry) + `
|
|
3346
|
+
`, "utf-8");
|
|
3347
|
+
} catch {}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3517
3351
|
// src/resolution/agent-resolver.ts
|
|
3518
3352
|
async function resolveAgentName(client, sessionId, cachedName) {
|
|
3519
3353
|
if (cachedName)
|
|
@@ -3553,6 +3387,43 @@ function resolveFallbackModel(chain, currentModel, health) {
|
|
|
3553
3387
|
return null;
|
|
3554
3388
|
}
|
|
3555
3389
|
|
|
3390
|
+
// src/preemptive.ts
|
|
3391
|
+
function tryPreemptiveRedirect(sessionId, modelKey2, agentName, store, config, logger) {
|
|
3392
|
+
const sessionState = store.sessions.get(sessionId);
|
|
3393
|
+
store.sessions.setOriginalModel(sessionId, modelKey2);
|
|
3394
|
+
if (sessionState.currentModel !== modelKey2) {
|
|
3395
|
+
const wasOnFallback = sessionState.currentModel !== null && sessionState.currentModel !== sessionState.originalModel;
|
|
3396
|
+
sessionState.currentModel = modelKey2;
|
|
3397
|
+
if (wasOnFallback && modelKey2 === sessionState.originalModel) {
|
|
3398
|
+
sessionState.fallbackDepth = 0;
|
|
3399
|
+
logger.debug("preemptive.depth.reset", { sessionId, modelKey: modelKey2 });
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
const health = store.health.get(modelKey2);
|
|
3403
|
+
if (health.state !== "rate_limited") {
|
|
3404
|
+
return { redirected: false };
|
|
3405
|
+
}
|
|
3406
|
+
const chain = resolveFallbackModels(config, agentName);
|
|
3407
|
+
if (chain.length === 0) {
|
|
3408
|
+
logger.debug("preemptive.no-chain", { sessionId, agentName });
|
|
3409
|
+
return { redirected: false };
|
|
3410
|
+
}
|
|
3411
|
+
const fallbackModel = resolveFallbackModel(chain, modelKey2, store.health);
|
|
3412
|
+
if (!fallbackModel) {
|
|
3413
|
+
logger.debug("preemptive.all-exhausted", { sessionId });
|
|
3414
|
+
return { redirected: false };
|
|
3415
|
+
}
|
|
3416
|
+
logger.info("preemptive.redirect", {
|
|
3417
|
+
sessionId,
|
|
3418
|
+
agentName,
|
|
3419
|
+
agentFile: sessionState.agentFile,
|
|
3420
|
+
from: modelKey2,
|
|
3421
|
+
to: fallbackModel
|
|
3422
|
+
});
|
|
3423
|
+
store.sessions.recordPreemptiveRedirect(sessionId, modelKey2, fallbackModel, agentName);
|
|
3424
|
+
return { redirected: true, fallbackModel };
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3556
3427
|
// src/replay/message-converter.ts
|
|
3557
3428
|
function convertPartsForPrompt(parts) {
|
|
3558
3429
|
const result = [];
|
|
@@ -3735,66 +3606,230 @@ async function attemptFallback(sessionId, reason, client, store, config, logger,
|
|
|
3735
3606
|
});
|
|
3736
3607
|
return { success: false, error: "prompt failed" };
|
|
3737
3608
|
}
|
|
3738
|
-
const newDepth = sessionState.fallbackDepth + 1;
|
|
3739
|
-
store.sessions.recordFallback(sessionId, currentModel ?? fallbackModel, fallbackModel, reason, agentName);
|
|
3740
|
-
logger.info("fallback.success", {
|
|
3741
|
-
sessionId,
|
|
3742
|
-
agentName,
|
|
3743
|
-
agentFile: store.sessions.get(sessionId).agentFile,
|
|
3744
|
-
from: currentModel,
|
|
3745
|
-
to: fallbackModel,
|
|
3746
|
-
reason,
|
|
3747
|
-
depth: newDepth
|
|
3748
|
-
});
|
|
3749
|
-
return { success: true, fallbackModel };
|
|
3750
|
-
} finally {
|
|
3751
|
-
store.sessions.releaseLock(sessionId);
|
|
3609
|
+
const newDepth = sessionState.fallbackDepth + 1;
|
|
3610
|
+
store.sessions.recordFallback(sessionId, currentModel ?? fallbackModel, fallbackModel, reason, agentName);
|
|
3611
|
+
logger.info("fallback.success", {
|
|
3612
|
+
sessionId,
|
|
3613
|
+
agentName,
|
|
3614
|
+
agentFile: store.sessions.get(sessionId).agentFile,
|
|
3615
|
+
from: currentModel,
|
|
3616
|
+
to: fallbackModel,
|
|
3617
|
+
reason,
|
|
3618
|
+
depth: newDepth
|
|
3619
|
+
});
|
|
3620
|
+
return { success: true, fallbackModel };
|
|
3621
|
+
} finally {
|
|
3622
|
+
store.sessions.releaseLock(sessionId);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
function sanitizeParts(parts) {
|
|
3626
|
+
if (!Array.isArray(parts))
|
|
3627
|
+
return [];
|
|
3628
|
+
return parts.filter((part) => typeof part === "object" && part !== null && ("type" in part));
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
// src/state/model-health.ts
|
|
3632
|
+
class ModelHealthStore {
|
|
3633
|
+
store = new Map;
|
|
3634
|
+
timer = null;
|
|
3635
|
+
onTransition;
|
|
3636
|
+
constructor(opts) {
|
|
3637
|
+
this.onTransition = opts?.onTransition;
|
|
3638
|
+
this.timer = setInterval(() => this.tick(), 30000);
|
|
3639
|
+
}
|
|
3640
|
+
get(modelKey2) {
|
|
3641
|
+
return this.store.get(modelKey2) ?? this.newHealth(modelKey2);
|
|
3642
|
+
}
|
|
3643
|
+
markRateLimited(modelKey2, cooldownMs, retryOriginalAfterMs) {
|
|
3644
|
+
const now = Date.now();
|
|
3645
|
+
const existing = this.get(modelKey2);
|
|
3646
|
+
const health = {
|
|
3647
|
+
...existing,
|
|
3648
|
+
state: "rate_limited",
|
|
3649
|
+
lastFailure: now,
|
|
3650
|
+
failureCount: existing.failureCount + 1,
|
|
3651
|
+
cooldownExpiresAt: now + cooldownMs,
|
|
3652
|
+
retryOriginalAt: now + retryOriginalAfterMs
|
|
3653
|
+
};
|
|
3654
|
+
this.store.set(modelKey2, health);
|
|
3655
|
+
}
|
|
3656
|
+
isUsable(modelKey2) {
|
|
3657
|
+
const h = this.get(modelKey2);
|
|
3658
|
+
return h.state === "healthy" || h.state === "cooldown";
|
|
3659
|
+
}
|
|
3660
|
+
preferScore(modelKey2) {
|
|
3661
|
+
const state = this.get(modelKey2).state;
|
|
3662
|
+
if (state === "healthy")
|
|
3663
|
+
return 2;
|
|
3664
|
+
if (state === "cooldown")
|
|
3665
|
+
return 1;
|
|
3666
|
+
return 0;
|
|
3667
|
+
}
|
|
3668
|
+
getAll() {
|
|
3669
|
+
return Array.from(this.store.values());
|
|
3670
|
+
}
|
|
3671
|
+
destroy() {
|
|
3672
|
+
if (this.timer) {
|
|
3673
|
+
clearInterval(this.timer);
|
|
3674
|
+
this.timer = null;
|
|
3675
|
+
}
|
|
3752
3676
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3677
|
+
tick() {
|
|
3678
|
+
const now = Date.now();
|
|
3679
|
+
for (const [key, health] of this.store) {
|
|
3680
|
+
if (health.state === "rate_limited" && health.cooldownExpiresAt && now >= health.cooldownExpiresAt) {
|
|
3681
|
+
const next = { ...health, state: "cooldown" };
|
|
3682
|
+
this.store.set(key, next);
|
|
3683
|
+
this.onTransition?.(key, "rate_limited", "cooldown");
|
|
3684
|
+
} else if (health.state === "cooldown" && health.retryOriginalAt && now >= health.retryOriginalAt) {
|
|
3685
|
+
const next = {
|
|
3686
|
+
...health,
|
|
3687
|
+
state: "healthy",
|
|
3688
|
+
cooldownExpiresAt: null,
|
|
3689
|
+
retryOriginalAt: null
|
|
3690
|
+
};
|
|
3691
|
+
this.store.set(key, next);
|
|
3692
|
+
this.onTransition?.(key, "cooldown", "healthy");
|
|
3693
|
+
}
|
|
3770
3694
|
}
|
|
3771
|
-
}
|
|
3695
|
+
}
|
|
3696
|
+
newHealth(modelKey2) {
|
|
3697
|
+
return {
|
|
3698
|
+
modelKey: modelKey2,
|
|
3699
|
+
state: "healthy",
|
|
3700
|
+
lastFailure: null,
|
|
3701
|
+
failureCount: 0,
|
|
3702
|
+
cooldownExpiresAt: null,
|
|
3703
|
+
retryOriginalAt: null
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3772
3706
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3707
|
+
|
|
3708
|
+
// src/state/session-state.ts
|
|
3709
|
+
class SessionStateStore {
|
|
3710
|
+
store = new Map;
|
|
3711
|
+
get(sessionId) {
|
|
3712
|
+
let state = this.store.get(sessionId);
|
|
3713
|
+
if (!state) {
|
|
3714
|
+
state = this.newState(sessionId);
|
|
3715
|
+
this.store.set(sessionId, state);
|
|
3781
3716
|
}
|
|
3782
|
-
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3717
|
+
return state;
|
|
3718
|
+
}
|
|
3719
|
+
acquireLock(sessionId) {
|
|
3720
|
+
const state = this.get(sessionId);
|
|
3721
|
+
if (state.isProcessing)
|
|
3722
|
+
return false;
|
|
3723
|
+
state.isProcessing = true;
|
|
3724
|
+
return true;
|
|
3725
|
+
}
|
|
3726
|
+
releaseLock(sessionId) {
|
|
3727
|
+
const state = this.store.get(sessionId);
|
|
3728
|
+
if (state)
|
|
3729
|
+
state.isProcessing = false;
|
|
3730
|
+
}
|
|
3731
|
+
isInDedupWindow(sessionId, windowMs = 3000) {
|
|
3732
|
+
const state = this.get(sessionId);
|
|
3733
|
+
if (!state.lastFallbackAt)
|
|
3734
|
+
return false;
|
|
3735
|
+
return Date.now() - state.lastFallbackAt < windowMs;
|
|
3736
|
+
}
|
|
3737
|
+
recordFallback(sessionId, fromModel, toModel, reason, agentName) {
|
|
3738
|
+
const state = this.get(sessionId);
|
|
3739
|
+
const event = {
|
|
3740
|
+
at: Date.now(),
|
|
3741
|
+
fromModel,
|
|
3742
|
+
toModel,
|
|
3743
|
+
reason,
|
|
3744
|
+
sessionId,
|
|
3745
|
+
trigger: "reactive",
|
|
3746
|
+
agentName
|
|
3747
|
+
};
|
|
3748
|
+
state.currentModel = toModel;
|
|
3749
|
+
state.fallbackDepth++;
|
|
3750
|
+
state.lastFallbackAt = event.at;
|
|
3751
|
+
state.recoveryNotifiedForModel = null;
|
|
3752
|
+
state.fallbackHistory.push(event);
|
|
3753
|
+
if (agentName)
|
|
3754
|
+
state.agentName = agentName;
|
|
3755
|
+
}
|
|
3756
|
+
recordPreemptiveRedirect(sessionId, fromModel, toModel, agentName) {
|
|
3757
|
+
const state = this.get(sessionId);
|
|
3758
|
+
const event = {
|
|
3759
|
+
at: Date.now(),
|
|
3760
|
+
fromModel,
|
|
3761
|
+
toModel,
|
|
3762
|
+
reason: "rate_limit",
|
|
3763
|
+
sessionId,
|
|
3764
|
+
trigger: "preemptive",
|
|
3765
|
+
agentName
|
|
3766
|
+
};
|
|
3767
|
+
state.currentModel = toModel;
|
|
3768
|
+
state.recoveryNotifiedForModel = null;
|
|
3769
|
+
state.fallbackHistory.push(event);
|
|
3770
|
+
if (agentName)
|
|
3771
|
+
state.agentName = agentName;
|
|
3772
|
+
}
|
|
3773
|
+
setOriginalModel(sessionId, model) {
|
|
3774
|
+
const state = this.get(sessionId);
|
|
3775
|
+
if (!state.originalModel) {
|
|
3776
|
+
state.originalModel = model;
|
|
3777
|
+
state.currentModel = model;
|
|
3792
3778
|
}
|
|
3793
|
-
}
|
|
3779
|
+
}
|
|
3780
|
+
setAgentName(sessionId, agentName) {
|
|
3781
|
+
const state = this.get(sessionId);
|
|
3782
|
+
state.agentName = agentName;
|
|
3783
|
+
}
|
|
3784
|
+
setAgentFile(sessionId, agentFile) {
|
|
3785
|
+
const state = this.get(sessionId);
|
|
3786
|
+
state.agentFile = agentFile;
|
|
3787
|
+
}
|
|
3788
|
+
partialReset(sessionId) {
|
|
3789
|
+
const state = this.store.get(sessionId);
|
|
3790
|
+
if (!state)
|
|
3791
|
+
return;
|
|
3792
|
+
state.fallbackHistory = [];
|
|
3793
|
+
state.lastFallbackAt = null;
|
|
3794
|
+
state.isProcessing = false;
|
|
3795
|
+
}
|
|
3796
|
+
delete(sessionId) {
|
|
3797
|
+
this.store.delete(sessionId);
|
|
3798
|
+
}
|
|
3799
|
+
getAll() {
|
|
3800
|
+
return Array.from(this.store.values());
|
|
3801
|
+
}
|
|
3802
|
+
newState(sessionId) {
|
|
3803
|
+
return {
|
|
3804
|
+
sessionId,
|
|
3805
|
+
agentName: null,
|
|
3806
|
+
agentFile: null,
|
|
3807
|
+
originalModel: null,
|
|
3808
|
+
currentModel: null,
|
|
3809
|
+
fallbackDepth: 0,
|
|
3810
|
+
isProcessing: false,
|
|
3811
|
+
lastFallbackAt: null,
|
|
3812
|
+
fallbackHistory: [],
|
|
3813
|
+
recoveryNotifiedForModel: null
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3794
3816
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3817
|
+
|
|
3818
|
+
// src/state/store.ts
|
|
3819
|
+
class FallbackStore {
|
|
3820
|
+
health;
|
|
3821
|
+
sessions;
|
|
3822
|
+
constructor(_config, logger) {
|
|
3823
|
+
this.sessions = new SessionStateStore;
|
|
3824
|
+
this.health = new ModelHealthStore({
|
|
3825
|
+
onTransition: (modelKey2, from, to) => {
|
|
3826
|
+
logger.info("health.transition", { modelKey: modelKey2, from, to });
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
destroy() {
|
|
3831
|
+
this.health.destroy();
|
|
3832
|
+
}
|
|
3798
3833
|
}
|
|
3799
3834
|
|
|
3800
3835
|
// src/tools/fallback-status.ts
|
|
@@ -3973,43 +4008,6 @@ function getLastUserModelAndAgent(data) {
|
|
|
3973
4008
|
return null;
|
|
3974
4009
|
}
|
|
3975
4010
|
|
|
3976
|
-
// src/preemptive.ts
|
|
3977
|
-
function tryPreemptiveRedirect(sessionId, modelKey2, agentName, store, config, logger) {
|
|
3978
|
-
const sessionState = store.sessions.get(sessionId);
|
|
3979
|
-
store.sessions.setOriginalModel(sessionId, modelKey2);
|
|
3980
|
-
if (sessionState.currentModel !== modelKey2) {
|
|
3981
|
-
const wasOnFallback = sessionState.currentModel !== null && sessionState.currentModel !== sessionState.originalModel;
|
|
3982
|
-
sessionState.currentModel = modelKey2;
|
|
3983
|
-
if (wasOnFallback && modelKey2 === sessionState.originalModel) {
|
|
3984
|
-
sessionState.fallbackDepth = 0;
|
|
3985
|
-
logger.debug("preemptive.depth.reset", { sessionId, modelKey: modelKey2 });
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
|
-
const health = store.health.get(modelKey2);
|
|
3989
|
-
if (health.state !== "rate_limited") {
|
|
3990
|
-
return { redirected: false };
|
|
3991
|
-
}
|
|
3992
|
-
const chain = resolveFallbackModels(config, agentName);
|
|
3993
|
-
if (chain.length === 0) {
|
|
3994
|
-
logger.debug("preemptive.no-chain", { sessionId, agentName });
|
|
3995
|
-
return { redirected: false };
|
|
3996
|
-
}
|
|
3997
|
-
const fallbackModel = resolveFallbackModel(chain, modelKey2, store.health);
|
|
3998
|
-
if (!fallbackModel) {
|
|
3999
|
-
logger.debug("preemptive.all-exhausted", { sessionId });
|
|
4000
|
-
return { redirected: false };
|
|
4001
|
-
}
|
|
4002
|
-
logger.info("preemptive.redirect", {
|
|
4003
|
-
sessionId,
|
|
4004
|
-
agentName,
|
|
4005
|
-
agentFile: sessionState.agentFile,
|
|
4006
|
-
from: modelKey2,
|
|
4007
|
-
to: fallbackModel
|
|
4008
|
-
});
|
|
4009
|
-
store.sessions.recordPreemptiveRedirect(sessionId, modelKey2, fallbackModel, agentName);
|
|
4010
|
-
return { redirected: true, fallbackModel };
|
|
4011
|
-
}
|
|
4012
|
-
|
|
4013
4011
|
// src/plugin.ts
|
|
4014
4012
|
var createPlugin = async ({ client, directory }) => {
|
|
4015
4013
|
const { config, path: configPath, warnings, migrated } = loadConfig(directory);
|