@alexeiled/pi-model-router 0.6.4 β 0.7.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/CHANGELOG.md +39 -0
- package/README.md +56 -183
- package/extensions/classifier.ts +4 -5
- package/extensions/commands.ts +280 -572
- package/extensions/config.ts +100 -9
- package/extensions/constants.ts +17 -0
- package/extensions/context.ts +185 -0
- package/extensions/jev.ts +355 -80
- package/extensions/provider.ts +33 -14
- package/extensions/state.ts +55 -3
- package/extensions/types.ts +82 -4
- package/extensions/ui.ts +62 -9
- package/model-router.example.json +9 -1
- package/package.json +1 -1
package/extensions/state.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
parseCanonicalModelRef,
|
|
9
9
|
} from './config';
|
|
10
10
|
import type {
|
|
11
|
+
JevContextMetrics,
|
|
11
12
|
JevDiagnostics,
|
|
12
13
|
PersistedStateInput,
|
|
13
14
|
RouterLastProfileState,
|
|
@@ -15,7 +16,14 @@ import type {
|
|
|
15
16
|
RouterPinByProfile,
|
|
16
17
|
RoutingDecision,
|
|
17
18
|
} from './types';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
isAdvisorOutcome,
|
|
21
|
+
isBypassReason,
|
|
22
|
+
isRoutingReasonCode,
|
|
23
|
+
JEV_OUTCOMES,
|
|
24
|
+
JEV_RESPONSE_ISSUES,
|
|
25
|
+
JEV_SELECTION_BASES,
|
|
26
|
+
} from './types';
|
|
19
27
|
|
|
20
28
|
const LAST_PROFILE_STATE_FILE = 'model-router-state.json';
|
|
21
29
|
|
|
@@ -144,12 +152,35 @@ export const isRouterPersistedState = (
|
|
|
144
152
|
);
|
|
145
153
|
};
|
|
146
154
|
|
|
155
|
+
const snapshotContextMetrics = (
|
|
156
|
+
value: unknown,
|
|
157
|
+
): JevContextMetrics | undefined => {
|
|
158
|
+
if (!isObjectRecord(value)) return undefined;
|
|
159
|
+
const result: JevContextMetrics = {
|
|
160
|
+
currentRequestTokens: 0,
|
|
161
|
+
historyTokens: 0,
|
|
162
|
+
toolTokens: 0,
|
|
163
|
+
historyTurns: 0,
|
|
164
|
+
toolResults: 0,
|
|
165
|
+
truncatedBlocks: 0,
|
|
166
|
+
};
|
|
167
|
+
for (const key of Object.keys(result) as (keyof JevContextMetrics)[]) {
|
|
168
|
+
const number = value[key];
|
|
169
|
+
if (!isFiniteNumber(number) || !Number.isSafeInteger(number) || number < 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
result[key] = number;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
};
|
|
175
|
+
|
|
147
176
|
const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
|
|
148
177
|
if (!isObjectRecord(value)) return undefined;
|
|
149
178
|
const outcome = JEV_OUTCOMES.find((entry) => entry === value.outcome);
|
|
150
179
|
if (!outcome || !isFiniteNumber(value.latencyMs) || value.latencyMs < 0)
|
|
151
180
|
return undefined;
|
|
152
181
|
const result: JevDiagnostics = { outcome, latencyMs: value.latencyMs };
|
|
182
|
+
const context = snapshotContextMetrics(value.context);
|
|
183
|
+
if (context) result.context = context;
|
|
153
184
|
if (
|
|
154
185
|
typeof value.requestId === 'string' &&
|
|
155
186
|
/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(value.requestId)
|
|
@@ -167,17 +198,35 @@ const snapshotJev = (value: unknown): JevDiagnostics | undefined => {
|
|
|
167
198
|
result.resolvedModel = value.resolvedModel;
|
|
168
199
|
if (isRouterTier(value.choice) || value.choice === 'uncertain')
|
|
169
200
|
result.choice = value.choice;
|
|
170
|
-
|
|
201
|
+
if (isRouterTier(value.selectedTier))
|
|
202
|
+
result.selectedTier = value.selectedTier;
|
|
203
|
+
const basis = JEV_SELECTION_BASES.find(
|
|
204
|
+
(entry) => entry === value.selectionBasis,
|
|
205
|
+
);
|
|
206
|
+
if (basis) result.selectionBasis = basis;
|
|
207
|
+
for (const key of [
|
|
208
|
+
'confidence',
|
|
209
|
+
'probability',
|
|
210
|
+
'routeProbability',
|
|
211
|
+
'threshold',
|
|
212
|
+
'probabilityThreshold',
|
|
213
|
+
] as const) {
|
|
171
214
|
const number = value[key];
|
|
172
215
|
if (isFiniteNumber(number) && number >= 0 && number <= 1)
|
|
173
216
|
result[key] = number;
|
|
174
217
|
}
|
|
218
|
+
const issue = JEV_RESPONSE_ISSUES.find(
|
|
219
|
+
(entry) => entry === value.responseIssue,
|
|
220
|
+
);
|
|
221
|
+
if (issue) result.responseIssue = issue;
|
|
175
222
|
for (const key of [
|
|
176
223
|
'startedAt',
|
|
177
224
|
'timeoutMs',
|
|
178
225
|
'candidateCount',
|
|
179
|
-
'
|
|
226
|
+
'estimatedInputTokens',
|
|
227
|
+
'actualInputTokens',
|
|
180
228
|
'httpStatus',
|
|
229
|
+
'attempts',
|
|
181
230
|
] as const) {
|
|
182
231
|
const number = value[key];
|
|
183
232
|
if (isFiniteNumber(number) && Number.isSafeInteger(number) && number >= 0)
|
|
@@ -209,6 +258,9 @@ export const snapshotDecision = (
|
|
|
209
258
|
? decision.errorClass
|
|
210
259
|
: undefined,
|
|
211
260
|
advisor: isAdvisorOutcome(decision.advisor) ? decision.advisor : undefined,
|
|
261
|
+
bypassReason: isBypassReason(decision.bypassReason)
|
|
262
|
+
? decision.bypassReason
|
|
263
|
+
: undefined,
|
|
212
264
|
jev: snapshotJev(decision.jev),
|
|
213
265
|
reuse:
|
|
214
266
|
decision.reuse === 'same-turn' ||
|
package/extensions/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
|
|
2
|
+
import type { Context } from '@earendil-works/pi-ai';
|
|
2
3
|
|
|
3
4
|
// Descending routing complexity; all tier iteration and ranking derives here.
|
|
4
5
|
export const ROUTER_TIERS = ['high', 'medium', 'low', 'micro'] as const;
|
|
@@ -21,6 +22,8 @@ export interface ModelDefinition {
|
|
|
21
22
|
export interface ClassifierConfig {
|
|
22
23
|
model: string;
|
|
23
24
|
thinking?: ThinkingLevel | undefined;
|
|
25
|
+
/** Total classifier budget in ms; `DEFAULT_CLASSIFIER_TIMEOUT_MS` when omitted. */
|
|
26
|
+
timeoutMs?: number | undefined;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface RoutedTierConfig {
|
|
@@ -40,6 +43,36 @@ export interface RoutedTierConfig {
|
|
|
40
43
|
resolvedThinkingLevels?: ThinkingLevel[] | undefined;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
export interface JevContextConfig {
|
|
47
|
+
previousTurns: number;
|
|
48
|
+
maxHistoryTokens: number;
|
|
49
|
+
toolResults: 'none' | 'last' | 'last-error';
|
|
50
|
+
maxToolTokens: number;
|
|
51
|
+
}
|
|
52
|
+
export interface JevRetryConfig {
|
|
53
|
+
/** HTTP attempts in total; 1 disables retries. */
|
|
54
|
+
maxAttempts: number;
|
|
55
|
+
/** First backoff delay; doubled per attempt, raised by `Retry-After`. */
|
|
56
|
+
backoffMs: number;
|
|
57
|
+
}
|
|
58
|
+
export interface JevTextExcerpt {
|
|
59
|
+
text: string;
|
|
60
|
+
truncated: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface JevContextState {
|
|
63
|
+
currentRequest: JevTextExcerpt;
|
|
64
|
+
recentDialogue: (JevTextExcerpt & { role: 'user' | 'assistant' })[];
|
|
65
|
+
recentToolEvidence: (JevTextExcerpt & { isError: boolean })[];
|
|
66
|
+
}
|
|
67
|
+
export interface JevContextMetrics {
|
|
68
|
+
currentRequestTokens: number;
|
|
69
|
+
historyTokens: number;
|
|
70
|
+
toolTokens: number;
|
|
71
|
+
historyTurns: number;
|
|
72
|
+
toolResults: number;
|
|
73
|
+
truncatedBlocks: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
export interface JevConfig {
|
|
44
77
|
enabled: boolean;
|
|
45
78
|
apiKey: string;
|
|
@@ -47,7 +80,10 @@ export interface JevConfig {
|
|
|
47
80
|
model: string;
|
|
48
81
|
timeoutMs: number;
|
|
49
82
|
confidenceThreshold: number;
|
|
50
|
-
|
|
83
|
+
probabilityThreshold: number;
|
|
84
|
+
maxStateTokens: number;
|
|
85
|
+
context?: JevContextConfig | undefined;
|
|
86
|
+
retry?: JevRetryConfig | undefined;
|
|
51
87
|
mode: 'advisory';
|
|
52
88
|
}
|
|
53
89
|
|
|
@@ -104,27 +140,46 @@ export interface JevDependencies {
|
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
export interface JevRequest {
|
|
107
|
-
|
|
143
|
+
context: Context;
|
|
108
144
|
candidates: readonly JevRouteCandidate[];
|
|
109
145
|
profile: JevProfileConfig | undefined;
|
|
146
|
+
/** Local fallback tier; abstention mass is assigned to it, never inferred remotely. */
|
|
147
|
+
baselineTier: RouterTier;
|
|
110
148
|
/** Absolute monotonic deadline supplied by the routing orchestrator. */
|
|
111
149
|
routingDeadline: number;
|
|
112
150
|
signal?: AbortSignal | undefined;
|
|
113
151
|
}
|
|
114
152
|
|
|
153
|
+
export const JEV_SELECTION_BASES = ['choice', 'probability'] as const;
|
|
154
|
+
export type JevSelectionBasis = (typeof JEV_SELECTION_BASES)[number];
|
|
155
|
+
|
|
156
|
+
/** Local validation codes; remote error text is never retained. */
|
|
157
|
+
export const JEV_RESPONSE_ISSUES = [
|
|
158
|
+
'unreadable-body',
|
|
159
|
+
'missing-answer',
|
|
160
|
+
'unexpected-answer-type',
|
|
161
|
+
'unknown-choice',
|
|
162
|
+
'invalid-confidence',
|
|
163
|
+
'distribution-keys',
|
|
164
|
+
'distribution-sum',
|
|
165
|
+
'distribution-argmax',
|
|
166
|
+
] as const;
|
|
167
|
+
export type JevResponseIssue = (typeof JEV_RESPONSE_ISSUES)[number];
|
|
168
|
+
|
|
115
169
|
export const JEV_OUTCOMES = [
|
|
116
170
|
'selected',
|
|
117
171
|
'uncertain',
|
|
118
|
-
'low-confidence',
|
|
119
172
|
'invalid-response',
|
|
120
173
|
'http-error',
|
|
121
174
|
'network-error',
|
|
122
175
|
'deadline',
|
|
123
176
|
'cancelled',
|
|
124
177
|
'unavailable',
|
|
178
|
+
'input-too-large',
|
|
125
179
|
] as const;
|
|
126
180
|
export type JevOutcome = (typeof JEV_OUTCOMES)[number];
|
|
127
181
|
export interface JevDiagnostics {
|
|
182
|
+
context?: JevContextMetrics | undefined;
|
|
128
183
|
/** Locally generated per HTTP request, shared by reusers; never supplied by Jev. */
|
|
129
184
|
requestId?: string | undefined;
|
|
130
185
|
outcome: JevOutcome;
|
|
@@ -133,13 +188,23 @@ export interface JevDiagnostics {
|
|
|
133
188
|
model?: string | undefined;
|
|
134
189
|
resolvedModel?: string | undefined;
|
|
135
190
|
choice?: RouterTier | 'uncertain' | undefined;
|
|
191
|
+
/** Acted-on tier, which a conservative probability selection can raise above `choice`. */
|
|
192
|
+
selectedTier?: RouterTier | undefined;
|
|
193
|
+
selectionBasis?: JevSelectionBasis | undefined;
|
|
136
194
|
confidence?: number | undefined;
|
|
137
195
|
probability?: number | undefined;
|
|
196
|
+
/** Cumulative probability of the selected tier and every lower tier. */
|
|
197
|
+
routeProbability?: number | undefined;
|
|
138
198
|
threshold?: number | undefined;
|
|
199
|
+
probabilityThreshold?: number | undefined;
|
|
139
200
|
timeoutMs?: number | undefined;
|
|
140
201
|
candidateCount?: number | undefined;
|
|
141
|
-
|
|
202
|
+
estimatedInputTokens?: number | undefined;
|
|
203
|
+
actualInputTokens?: number | undefined;
|
|
142
204
|
httpStatus?: number | undefined;
|
|
205
|
+
/** HTTP attempts made, including the retry of a documented transient status. */
|
|
206
|
+
attempts?: number | undefined;
|
|
207
|
+
responseIssue?: JevResponseIssue | undefined;
|
|
143
208
|
}
|
|
144
209
|
export interface JevResult {
|
|
145
210
|
advice?: JevAdvice | undefined;
|
|
@@ -195,6 +260,18 @@ export const ADVISOR_OUTCOMES = [
|
|
|
195
260
|
export type AdvisorOutcome = (typeof ADVISOR_OUTCOMES)[number];
|
|
196
261
|
export const isAdvisorOutcome = (value: unknown): value is AdvisorOutcome =>
|
|
197
262
|
ADVISOR_OUTCOMES.some((outcome) => outcome === value);
|
|
263
|
+
/** Why a configured advisor was not asked on this decision. */
|
|
264
|
+
export const BYPASS_REASONS = [
|
|
265
|
+
'pinned',
|
|
266
|
+
'budget',
|
|
267
|
+
'single-candidate',
|
|
268
|
+
'tool-continuation',
|
|
269
|
+
'no-user-turn',
|
|
270
|
+
'turn-advised',
|
|
271
|
+
] as const;
|
|
272
|
+
export type BypassReason = (typeof BYPASS_REASONS)[number];
|
|
273
|
+
export const isBypassReason = (value: unknown): value is BypassReason =>
|
|
274
|
+
BYPASS_REASONS.some((reason) => reason === value);
|
|
198
275
|
|
|
199
276
|
export interface RoutingDecision {
|
|
200
277
|
profile: string;
|
|
@@ -207,6 +284,7 @@ export interface RoutingDecision {
|
|
|
207
284
|
routingLatencyMs?: number | undefined;
|
|
208
285
|
errorClass?: RoutingErrorClass | undefined;
|
|
209
286
|
advisor?: AdvisorOutcome | undefined;
|
|
287
|
+
bypassReason?: BypassReason | undefined;
|
|
210
288
|
jev?: JevDiagnostics | undefined;
|
|
211
289
|
reuse?: 'same-turn' | 'shared' | 'continuation' | undefined;
|
|
212
290
|
thinking: ThinkingLevel;
|
package/extensions/ui.ts
CHANGED
|
@@ -25,6 +25,25 @@ export const formatDecisionSource = (decision: RoutingDecision): string =>
|
|
|
25
25
|
? decision.reasonCode
|
|
26
26
|
: '';
|
|
27
27
|
|
|
28
|
+
const formatBypassReason = (decision: RoutingDecision): string => {
|
|
29
|
+
switch (decision.bypassReason) {
|
|
30
|
+
case 'pinned':
|
|
31
|
+
return `pinned ${decision.tier}`;
|
|
32
|
+
case 'budget':
|
|
33
|
+
return 'over budget';
|
|
34
|
+
case 'single-candidate':
|
|
35
|
+
return `only ${decision.tier} eligible`;
|
|
36
|
+
case 'tool-continuation':
|
|
37
|
+
return 'tool turn';
|
|
38
|
+
case 'no-user-turn':
|
|
39
|
+
return 'no user turn';
|
|
40
|
+
case 'turn-advised':
|
|
41
|
+
return 'turn already advised';
|
|
42
|
+
default:
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
export const formatAdvisorLabel = (
|
|
29
48
|
decision: RoutingDecision,
|
|
30
49
|
): string | undefined => {
|
|
@@ -32,8 +51,10 @@ export const formatAdvisorLabel = (
|
|
|
32
51
|
switch (decision.advisor) {
|
|
33
52
|
case 'none':
|
|
34
53
|
return 'local baseline';
|
|
35
|
-
case 'bypassed':
|
|
36
|
-
|
|
54
|
+
case 'bypassed': {
|
|
55
|
+
const reason = formatBypassReason(decision);
|
|
56
|
+
return reason ? `advice skipped: ${reason}` : 'advice bypassed';
|
|
57
|
+
}
|
|
37
58
|
case 'jev':
|
|
38
59
|
return 'π§ Jev β';
|
|
39
60
|
case 'jev-fallback':
|
|
@@ -76,6 +97,11 @@ export const formatAdvisorDetail = (
|
|
|
76
97
|
);
|
|
77
98
|
if (metrics.choice && metrics.choice !== 'uncertain')
|
|
78
99
|
parts.push(`choice=${metrics.choice}`);
|
|
100
|
+
if (metrics.selectedTier && metrics.selectedTier !== metrics.choice)
|
|
101
|
+
parts.push(`selected=${metrics.selectedTier}`);
|
|
102
|
+
if (metrics.selectionBasis) parts.push(`basis=${metrics.selectionBasis}`);
|
|
103
|
+
if (metrics.routeProbability !== undefined)
|
|
104
|
+
parts.push(`route-p=${(metrics.routeProbability * 100).toFixed(1)}%`);
|
|
79
105
|
if (metrics.probability !== undefined)
|
|
80
106
|
parts.push(
|
|
81
107
|
`${metrics.outcome === 'uncertain' ? 'abstention-p' : 'p'}=${(metrics.probability * 100).toFixed(1)}%`,
|
|
@@ -86,14 +112,38 @@ export const formatAdvisorDetail = (
|
|
|
86
112
|
);
|
|
87
113
|
if (metrics.threshold !== undefined && metrics.outcome !== 'uncertain')
|
|
88
114
|
parts.push(`threshold=${(metrics.threshold * 100).toFixed(1)}%`);
|
|
115
|
+
if (
|
|
116
|
+
metrics.probabilityThreshold !== undefined &&
|
|
117
|
+
metrics.selectionBasis === 'probability'
|
|
118
|
+
)
|
|
119
|
+
parts.push(
|
|
120
|
+
`route-threshold=${(metrics.probabilityThreshold * 100).toFixed(1)}%`,
|
|
121
|
+
);
|
|
89
122
|
if (metrics.timeoutMs !== undefined)
|
|
90
123
|
parts.push(`budget=${metrics.timeoutMs}ms`);
|
|
91
124
|
if (metrics.candidateCount !== undefined)
|
|
92
125
|
parts.push(`candidates=${metrics.candidateCount}`);
|
|
93
|
-
if (metrics.
|
|
94
|
-
|
|
126
|
+
if (metrics.context) {
|
|
127
|
+
const context = metrics.context;
|
|
128
|
+
parts.push(
|
|
129
|
+
`stateβ${context.currentRequestTokens + context.historyTokens + context.toolTokens} tokens: ${context.currentRequestTokens} current + ${context.historyTokens} dialogue/${context.historyTurns} turns + ${context.toolTokens} tool/${context.toolResults} results; truncated=${context.truncatedBlocks}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (metrics.estimatedInputTokens !== undefined)
|
|
133
|
+
parts.push(`requestβ${metrics.estimatedInputTokens} tokens`);
|
|
134
|
+
if (metrics.actualInputTokens !== undefined)
|
|
135
|
+
parts.push(`Jev usage=${metrics.actualInputTokens} input tokens`);
|
|
95
136
|
if (metrics.httpStatus !== undefined)
|
|
96
137
|
parts.push(`HTTP ${metrics.httpStatus}`);
|
|
138
|
+
if (metrics.attempts !== undefined && metrics.attempts > 1)
|
|
139
|
+
parts.push(`attempts=${metrics.attempts}`);
|
|
140
|
+
if (metrics.responseIssue) parts.push(`response=${metrics.responseIssue}`);
|
|
141
|
+
if (metrics.httpStatus === 401)
|
|
142
|
+
parts.push('Check the user-config Jev API key.');
|
|
143
|
+
if (metrics.httpStatus === 422)
|
|
144
|
+
parts.push('Jev rejected the request shape.');
|
|
145
|
+
if (metrics.httpStatus === 429 || metrics.httpStatus === 529)
|
|
146
|
+
parts.push('Transient Jev limit; the router retried once within budget.');
|
|
97
147
|
} else if (decision.errorClass) {
|
|
98
148
|
parts.push(decision.errorClass);
|
|
99
149
|
}
|
|
@@ -119,10 +169,10 @@ export const formatAdvisorFooter = (
|
|
|
119
169
|
let summary: string;
|
|
120
170
|
switch (metrics.outcome) {
|
|
121
171
|
case 'selected':
|
|
122
|
-
summary =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
172
|
+
summary =
|
|
173
|
+
metrics.selectionBasis === 'probability'
|
|
174
|
+
? `${metrics.choice ?? 'choice'}${confidence}${metrics.threshold !== undefined ? ` <${Math.round(metrics.threshold * 100)}%` : ''} β ${metrics.selectedTier ?? decision.tier}`
|
|
175
|
+
: `β ${metrics.choice ?? decision.tier}${confidence}`;
|
|
126
176
|
break;
|
|
127
177
|
case 'uncertain':
|
|
128
178
|
summary = ': no tier chosen β baseline';
|
|
@@ -137,7 +187,7 @@ export const formatAdvisorFooter = (
|
|
|
137
187
|
summary = ': network error β baseline';
|
|
138
188
|
break;
|
|
139
189
|
case 'invalid-response':
|
|
140
|
-
summary =
|
|
190
|
+
summary = `: invalid response${metrics.responseIssue ? ` (${metrics.responseIssue})` : ''} β baseline`;
|
|
141
191
|
break;
|
|
142
192
|
case 'cancelled':
|
|
143
193
|
summary = ': cancelled';
|
|
@@ -145,6 +195,9 @@ export const formatAdvisorFooter = (
|
|
|
145
195
|
case 'unavailable':
|
|
146
196
|
summary = `: ${metrics.choice ? 'target' : 'advice'} unavailable β baseline`;
|
|
147
197
|
break;
|
|
198
|
+
case 'input-too-large':
|
|
199
|
+
summary = ': estimated request too large β baseline';
|
|
200
|
+
break;
|
|
148
201
|
}
|
|
149
202
|
const latency =
|
|
150
203
|
metrics.latencyMs >= 1000
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
"model": "jev-1.13.0",
|
|
9
9
|
"timeoutMs": 1500,
|
|
10
10
|
"confidenceThreshold": 0.65,
|
|
11
|
-
"
|
|
11
|
+
"probabilityThreshold": 0.8,
|
|
12
|
+
"maxStateTokens": 3000,
|
|
13
|
+
"context": {
|
|
14
|
+
"previousTurns": 2,
|
|
15
|
+
"maxHistoryTokens": 500,
|
|
16
|
+
"toolResults": "last-error",
|
|
17
|
+
"maxToolTokens": 250
|
|
18
|
+
},
|
|
19
|
+
"retry": { "maxAttempts": 2, "backoffMs": 400 },
|
|
12
20
|
"mode": "advisory"
|
|
13
21
|
},
|
|
14
22
|
"classifierModel": "flash",
|