@lazyingart/agintiflow 0.20.251 → 0.20.252
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/docs/model-selection.md +2 -0
- package/package.json +3 -2
- package/src/agent-runner.js +110 -1
- package/src/model-client.js +32 -8
- package/src/provider-handoff.js +126 -0
package/docs/model-selection.md
CHANGED
|
@@ -26,6 +26,8 @@ The local tier policy uses the workstation's installed aliases without treating
|
|
|
26
26
|
|
|
27
27
|
Installed aliases are not loaded during routing. A genuine coding/implementation request starts on Deep, checks the authenticated model inventory, and selects the exact `AGINTI_LOCALLLM_CODE_MODEL` value only when present. Missing or unverified capability stays on Deep; the decision and effective model are recorded so fallback sessions can re-evaluate and selected sessions resume on the same model. Explanation-only code questions and non-code writing/research/documentation/design keep their existing route. Automatic Max is off by default; set `AGINTI_LOCALLLM_ALLOW_AUTO_MAX=true` to opt in. An opted-in high-complexity non-code run starts on Deep, confirms the Max alias through authenticated `/v1/models`, and only then samples current resources. Unknown or pressured resource state stays on Deep. Explicit Max also cannot bypass the live gate: immediately before client creation, each new or resumed run rechecks at least 24 GiB available RAM, swap use at or below 75%, and 40 GiB aggregate free NVIDIA memory. A blocked explicit gate creates no model client and performs no inference.
|
|
28
28
|
|
|
29
|
+
Smart hosted routes fail over only toward the local trust boundary. If a hosted provider returns an authentication, quota, rate-limit, model-availability, capacity, or network failure, AgInTiFlow records `provider.handoff_requested`, patches the same durable session to `localllm-deep`, verifies LocalLLM readiness, records `provider.handoff_activated`, and continues without replaying the user request or prior side effects. Manual routing remains exact and never changes provider automatically. Retained integration profiles also keep their pinned provider identity. Set `AGINTI_PROVIDER_HANDOFF=false` to disable the smart hosted-to-local handoff or `AGINTI_PROVIDER_HANDOFF_MODEL` to select another verified local fallback model.
|
|
30
|
+
|
|
29
31
|
Long writing tasks use an additional tool boundary: `writing_specialist`. The main model still plans the run, manages files, formats Markdown/LaTeX/Final Draft output, compiles/checks artifacts, and finishes. The specialist gets only the writing brief, canon, style guide, prior draft, target, audience, constraints, and format intent, then returns prose plus a formatter handoff. It follows the active LocalLLM provider by default even when hosted keys exist. Cross-provider writing requires both an explicit target (`AGINTI_WRITING_PROVIDER` or a per-run provider override) and `AGINTI_ALLOW_HOSTED_WRITING_SPECIALIST=true` (or the equivalent per-run permission flag); language detection and ambient credentials never grant that permission.
|
|
30
32
|
|
|
31
33
|
## CLI Commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.252",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
"smoke:localllm-code-routing": "node scripts/smoke-localllm-code-routing.js",
|
|
175
175
|
"smoke:local-failure-recovery": "node scripts/smoke-local-failure-recovery.js",
|
|
176
176
|
"smoke:planning-timeout-recovery": "node scripts/smoke-planning-timeout-recovery.js",
|
|
177
|
+
"smoke:provider-handoff": "node scripts/smoke-provider-handoff.js",
|
|
177
178
|
"smoke:local-resource-policy": "node scripts/smoke-local-resource-policy.js",
|
|
178
179
|
"smoke:context-budget-recovery": "node scripts/smoke-context-budget-recovery.js",
|
|
179
180
|
"smoke:localllm-model-tiers": "node scripts/smoke-localllm-model-tiers.js",
|
|
@@ -228,7 +229,7 @@
|
|
|
228
229
|
"publish:env": "node scripts/npm-publish-from-env.js publish --access public",
|
|
229
230
|
"publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
|
|
230
231
|
"pretest": "npm run smoke:planning-timeout-recovery",
|
|
231
|
-
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:integration-production-runtime-bundle && npm run smoke:integration-retained-idempotency-store && npm run smoke:integration-storage-authority && npm run smoke:integration-retained-durable-common && npm run smoke:integration-retained-file-lock && npm run smoke:integration-retained-event-ledger && npm run smoke:integration-retained-event-ledger-bundle && npm run smoke:integration-retained-repository-kernel && npm run smoke:integration-retained-session-state-store && npm run smoke:integration-retained-native-session-repository-state && npm run smoke:integration-retained-native-execution-evidence && npm run smoke:integration-retained-runtime-repository-surface && npm run smoke:integration-retained-runtime-repository-phase-b && npm run smoke:integration-runtime-repository-contract && npm run smoke:integration-runtime-authority && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:document-artifact-quality && npm run smoke:writing-specialist-routing && npm run smoke:workspace-secret-false-positive && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
232
|
+
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:integration-production-runtime-bundle && npm run smoke:integration-retained-idempotency-store && npm run smoke:integration-storage-authority && npm run smoke:integration-retained-durable-common && npm run smoke:integration-retained-file-lock && npm run smoke:integration-retained-event-ledger && npm run smoke:integration-retained-event-ledger-bundle && npm run smoke:integration-retained-repository-kernel && npm run smoke:integration-retained-session-state-store && npm run smoke:integration-retained-native-session-repository-state && npm run smoke:integration-retained-native-execution-evidence && npm run smoke:integration-retained-runtime-repository-surface && npm run smoke:integration-retained-runtime-repository-phase-b && npm run smoke:integration-runtime-repository-contract && npm run smoke:integration-runtime-authority && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:document-artifact-quality && npm run smoke:writing-specialist-routing && npm run smoke:workspace-secret-false-positive && npm run smoke:provider-handoff && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
232
233
|
"pack:dry-run": "npm pack --dry-run",
|
|
233
234
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
234
235
|
},
|
package/src/agent-runner.js
CHANGED
|
@@ -93,6 +93,7 @@ import { longJobStatus, startLongJob } from "./long-job-tools.js";
|
|
|
93
93
|
import { executeAgentLinkTool, isAgentLinkTool } from "./agentlink.js";
|
|
94
94
|
import { classifyGoalIntent, isDirectAnswerIntent } from "./goal-intent.js";
|
|
95
95
|
import { normalizeProviderBaseURL, normalizeProviderId, providerRequiresApiKey } from "./provider-contract.js";
|
|
96
|
+
import { resolveProviderHandoff } from "./provider-handoff.js";
|
|
96
97
|
import { ProviderReadinessError, probeProviderRuntime } from "./provider-runtime.js";
|
|
97
98
|
import { probeLocalMaxResources } from "./local-resource-policy.js";
|
|
98
99
|
import {
|
|
@@ -11058,7 +11059,78 @@ async function recordPreInferenceFailure({ error, config, state, store, observer
|
|
|
11058
11059
|
emitConsole(config, result, { kind: "error", error: true });
|
|
11059
11060
|
}
|
|
11060
11061
|
|
|
11061
|
-
|
|
11062
|
+
class ProviderHandoffSignal extends Error {
|
|
11063
|
+
constructor({ sessionId, expectedRuntimeRevision, decision }) {
|
|
11064
|
+
super(`Provider handoff requested for ${decision.sourceProvider} -> ${decision.targetProvider}.`);
|
|
11065
|
+
this.name = "ProviderHandoffSignal";
|
|
11066
|
+
this.sessionId = sessionId;
|
|
11067
|
+
this.expectedRuntimeRevision = expectedRuntimeRevision;
|
|
11068
|
+
this.decision = decision;
|
|
11069
|
+
}
|
|
11070
|
+
}
|
|
11071
|
+
|
|
11072
|
+
async function prepareProviderHandoff({ error, config, state, store, observers, sessionId, stage = "runtime" }) {
|
|
11073
|
+
const decision = resolveProviderHandoff(error, config, { stage });
|
|
11074
|
+
if (!decision) return null;
|
|
11075
|
+
|
|
11076
|
+
state.meta = state.meta || {};
|
|
11077
|
+
const priorAttempts = Number(state.meta.providerHandoff?.attempts || 0);
|
|
11078
|
+
if (priorAttempts >= 1) return null;
|
|
11079
|
+
|
|
11080
|
+
const at = new Date().toISOString();
|
|
11081
|
+
const detail = {
|
|
11082
|
+
version: 1,
|
|
11083
|
+
attempts: priorAttempts + 1,
|
|
11084
|
+
status: "pending",
|
|
11085
|
+
sourceProvider: decision.sourceProvider,
|
|
11086
|
+
sourceModel: decision.sourceModel,
|
|
11087
|
+
targetProvider: decision.targetProvider,
|
|
11088
|
+
targetModel: decision.targetModel,
|
|
11089
|
+
failureCode: decision.failureCode,
|
|
11090
|
+
httpStatus: decision.status || 0,
|
|
11091
|
+
requestedAt: at,
|
|
11092
|
+
};
|
|
11093
|
+
state.meta.providerHandoff = detail;
|
|
11094
|
+
state.updatedAt = at;
|
|
11095
|
+
updateGoalStatus(state, "active", "provider_handoff", at);
|
|
11096
|
+
await store.saveState(state);
|
|
11097
|
+
await store.appendEvent("provider.handoff_requested", detail);
|
|
11098
|
+
observers.event("provider.handoff_requested", { ...detail, sessionId });
|
|
11099
|
+
emitConsole(
|
|
11100
|
+
config,
|
|
11101
|
+
`${decision.sourceProvider}/${decision.sourceModel} is unavailable (${decision.failureCode}); continuing the same session with ${decision.targetProvider}/${decision.targetModel}.`,
|
|
11102
|
+
{ kind: "meta" }
|
|
11103
|
+
);
|
|
11104
|
+
return new ProviderHandoffSignal({
|
|
11105
|
+
sessionId,
|
|
11106
|
+
expectedRuntimeRevision: Number(state.meta.runtimeConfig?.revision || 1),
|
|
11107
|
+
decision,
|
|
11108
|
+
});
|
|
11109
|
+
}
|
|
11110
|
+
|
|
11111
|
+
async function activatePendingProviderHandoff({ config, state, store, observers, sessionId }) {
|
|
11112
|
+
const handoff = state.meta?.providerHandoff;
|
|
11113
|
+
if (
|
|
11114
|
+
!handoff ||
|
|
11115
|
+
handoff.status !== "pending" ||
|
|
11116
|
+
normalizeProviderId(handoff.targetProvider, "") !== normalizeProviderId(config.provider, "") ||
|
|
11117
|
+
String(handoff.targetModel || "") !== String(config.model || "")
|
|
11118
|
+
) {
|
|
11119
|
+
return;
|
|
11120
|
+
}
|
|
11121
|
+
const detail = {
|
|
11122
|
+
...handoff,
|
|
11123
|
+
status: "active",
|
|
11124
|
+
activatedAt: new Date().toISOString(),
|
|
11125
|
+
};
|
|
11126
|
+
state.meta.providerHandoff = detail;
|
|
11127
|
+
state.updatedAt = detail.activatedAt;
|
|
11128
|
+
await store.saveState(state);
|
|
11129
|
+
await store.appendEvent("provider.handoff_activated", detail);
|
|
11130
|
+
observers.event("provider.handoff_activated", { ...detail, sessionId });
|
|
11131
|
+
}
|
|
11132
|
+
|
|
11133
|
+
async function runAgentOnce(config) {
|
|
11062
11134
|
assertIntegrationRunAgentInvocation(config);
|
|
11063
11135
|
const incomingConfig = config;
|
|
11064
11136
|
const sessionId = config.resume || config.sessionId || `web-agent-${crypto.randomUUID()}`;
|
|
@@ -11087,6 +11159,11 @@ export async function runAgent(config) {
|
|
|
11087
11159
|
state.meta = state.meta || {};
|
|
11088
11160
|
state.meta.runtimeConfig = runtime.snapshot;
|
|
11089
11161
|
config = rebuildResumedRuntimeConfig(incomingConfig, runtime.runtimeOverrides, sessionId);
|
|
11162
|
+
// Keep compatibility fields aligned with the authoritative runtime
|
|
11163
|
+
// snapshot. Provider/model patches otherwise leave stale top-level values
|
|
11164
|
+
// that can mislead later diagnostics and legacy session readers.
|
|
11165
|
+
state.provider = config.provider;
|
|
11166
|
+
state.model = config.model;
|
|
11090
11167
|
const patchedRuntimeFields = runtime.patched
|
|
11091
11168
|
? Object.keys(incomingConfig.runtimePatch || {}).filter((field) => isSessionRuntimeField(field))
|
|
11092
11169
|
: [];
|
|
@@ -11261,10 +11338,22 @@ export async function runAgent(config) {
|
|
|
11261
11338
|
}
|
|
11262
11339
|
client = config.clientFactory ? await config.clientFactory(config) : createClient(config);
|
|
11263
11340
|
} catch (error) {
|
|
11341
|
+
const handoff = await prepareProviderHandoff({
|
|
11342
|
+
error,
|
|
11343
|
+
config,
|
|
11344
|
+
state,
|
|
11345
|
+
store,
|
|
11346
|
+
observers,
|
|
11347
|
+
sessionId,
|
|
11348
|
+
stage: "preflight",
|
|
11349
|
+
});
|
|
11350
|
+
if (handoff) throw handoff;
|
|
11264
11351
|
await recordPreInferenceFailure({ error, config, state, store, observers, sessionId });
|
|
11265
11352
|
throw error;
|
|
11266
11353
|
}
|
|
11267
11354
|
|
|
11355
|
+
await activatePendingProviderHandoff({ config, state, store, observers, sessionId });
|
|
11356
|
+
|
|
11268
11357
|
ensureChatState(state);
|
|
11269
11358
|
|
|
11270
11359
|
const initialRepair = repairModelMessageHistory(state, config);
|
|
@@ -12937,6 +13026,9 @@ export async function runAgent(config) {
|
|
|
12937
13026
|
...goalRunMetadata(state),
|
|
12938
13027
|
};
|
|
12939
13028
|
} catch (error) {
|
|
13029
|
+
if (error instanceof ProviderHandoffSignal) throw error;
|
|
13030
|
+
const handoff = await prepareProviderHandoff({ error, config, state, store, observers, sessionId });
|
|
13031
|
+
if (handoff) throw handoff;
|
|
12940
13032
|
if (isModelTimeoutError(error)) {
|
|
12941
13033
|
const detail = {
|
|
12942
13034
|
reason: "model_timeout",
|
|
@@ -13000,3 +13092,20 @@ export async function runAgent(config) {
|
|
|
13000
13092
|
await flushHousekeeping();
|
|
13001
13093
|
}
|
|
13002
13094
|
}
|
|
13095
|
+
|
|
13096
|
+
export async function runAgent(config) {
|
|
13097
|
+
try {
|
|
13098
|
+
return await runAgentOnce(config);
|
|
13099
|
+
} catch (error) {
|
|
13100
|
+
if (!(error instanceof ProviderHandoffSignal)) throw error;
|
|
13101
|
+
const decision = error.decision;
|
|
13102
|
+
return runAgentOnce({
|
|
13103
|
+
...config,
|
|
13104
|
+
goal: "",
|
|
13105
|
+
resume: error.sessionId,
|
|
13106
|
+
sessionId: error.sessionId,
|
|
13107
|
+
runtimePatch: decision.runtimePatch,
|
|
13108
|
+
expectedRuntimeRevision: error.expectedRuntimeRevision,
|
|
13109
|
+
});
|
|
13110
|
+
}
|
|
13111
|
+
}
|
package/src/model-client.js
CHANGED
|
@@ -148,6 +148,22 @@ function shouldRetryWithoutReasoningEffort(error, payload = {}) {
|
|
|
148
148
|
return /reasoning[_\s.-]?effort|unsupported parameter|unknown parameter|unrecognized request argument/i.test(message);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
function annotateProviderRequestError(error, config = {}, label = "model request") {
|
|
152
|
+
if (!error || (typeof error !== "object" && typeof error !== "function")) return error;
|
|
153
|
+
try {
|
|
154
|
+
Object.defineProperties(error, {
|
|
155
|
+
agintiProviderRequest: { value: true, configurable: true },
|
|
156
|
+
agintiProvider: { value: String(config.provider || ""), configurable: true },
|
|
157
|
+
agintiProviderModel: { value: String(config.model || ""), configurable: true },
|
|
158
|
+
agintiProviderRequestLabel: { value: String(label || "model request"), configurable: true },
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// Some SDK errors can be non-extensible. Classification still has the
|
|
162
|
+
// explicit preflight path, while an unmarked runtime error fails normally.
|
|
163
|
+
}
|
|
164
|
+
return error;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
export async function createChatCompletion(client, payload, config, label = "model request") {
|
|
152
168
|
const preparedPayload = withChatReasoningEffort(payload, config);
|
|
153
169
|
const timeout = resolveModelTimeoutMs(config);
|
|
@@ -157,9 +173,13 @@ export async function createChatCompletion(client, payload, config, label = "mod
|
|
|
157
173
|
} catch (error) {
|
|
158
174
|
if (shouldRetryWithoutReasoningEffort(error, preparedPayload)) {
|
|
159
175
|
const { reasoning_effort: _reasoningEffort, ...retryPayload } = preparedPayload;
|
|
160
|
-
|
|
176
|
+
try {
|
|
177
|
+
return await client.chat.completions.create(retryPayload, requestOptions(config));
|
|
178
|
+
} catch (retryError) {
|
|
179
|
+
throw annotateProviderRequestError(retryError, config, label);
|
|
180
|
+
}
|
|
161
181
|
}
|
|
162
|
-
throw error;
|
|
182
|
+
throw annotateProviderRequestError(error, config, label);
|
|
163
183
|
}
|
|
164
184
|
}
|
|
165
185
|
|
|
@@ -196,18 +216,22 @@ export async function createChatCompletion(client, payload, config, label = "mod
|
|
|
196
216
|
} catch (error) {
|
|
197
217
|
if (shouldRetryWithoutReasoningEffort(error, preparedPayload)) {
|
|
198
218
|
const { reasoning_effort: _reasoningEffort, ...retryPayload } = preparedPayload;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
try {
|
|
220
|
+
return await client.chat.completions.create(retryPayload, {
|
|
221
|
+
...requestOptions(config),
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
});
|
|
224
|
+
} catch (retryError) {
|
|
225
|
+
throw annotateProviderRequestError(retryError, config, label);
|
|
226
|
+
}
|
|
203
227
|
}
|
|
204
228
|
if (timedOut && error?.name !== "ModelTimeoutError") {
|
|
205
229
|
const timeoutError = new Error(`${label} timed out after ${timeout}ms`);
|
|
206
230
|
timeoutError.name = "ModelTimeoutError";
|
|
207
231
|
timeoutError.cause = error;
|
|
208
|
-
throw timeoutError;
|
|
232
|
+
throw annotateProviderRequestError(timeoutError, config, label);
|
|
209
233
|
}
|
|
210
|
-
throw error;
|
|
234
|
+
throw annotateProviderRequestError(error, config, label);
|
|
211
235
|
} finally {
|
|
212
236
|
if (timer) clearTimeout(timer);
|
|
213
237
|
if (config.abortSignal) {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { LOCALLLM_MODEL_TIERS } from "./model-routing.js";
|
|
2
|
+
import { normalizeProviderId } from "./provider-contract.js";
|
|
3
|
+
|
|
4
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
5
|
+
"ECONNREFUSED",
|
|
6
|
+
"ECONNRESET",
|
|
7
|
+
"EHOSTUNREACH",
|
|
8
|
+
"ENETUNREACH",
|
|
9
|
+
"ENOTFOUND",
|
|
10
|
+
"EAI_AGAIN",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function enabled(value, fallback = true) {
|
|
14
|
+
if (value === undefined || value === null || String(value).trim() === "") return fallback;
|
|
15
|
+
return !/^(?:0|false|off|no)$/i.test(String(value).trim());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function providerErrorText(error) {
|
|
19
|
+
return [
|
|
20
|
+
error?.code,
|
|
21
|
+
error?.type,
|
|
22
|
+
error?.message,
|
|
23
|
+
error?.error?.code,
|
|
24
|
+
error?.error?.type,
|
|
25
|
+
error?.error?.message,
|
|
26
|
+
error?.response?.data?.error?.code,
|
|
27
|
+
error?.response?.data?.error?.type,
|
|
28
|
+
error?.response?.data?.error?.message,
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" ")
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function classifyProviderHandoffError(error) {
|
|
36
|
+
const status = Number(error?.status || error?.response?.status || 0);
|
|
37
|
+
const code = String(error?.code || "").trim().toUpperCase();
|
|
38
|
+
const text = providerErrorText(error);
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
status === 402 ||
|
|
42
|
+
/insufficient[_ -]?(?:quota|balance)|quota exceeded|billing|account balance|credits? exhausted/.test(text)
|
|
43
|
+
) {
|
|
44
|
+
return { eligible: true, code: "provider_quota", status };
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
code === "API_KEY_REQUIRED" ||
|
|
48
|
+
status === 401 ||
|
|
49
|
+
status === 403 ||
|
|
50
|
+
/authentication|unauthorized|invalid[_ -]?api[_ -]?key|permission denied/.test(text)
|
|
51
|
+
) {
|
|
52
|
+
return { eligible: true, code: "provider_auth", status };
|
|
53
|
+
}
|
|
54
|
+
if (status === 429 || /rate[_ -]?limit|too many requests/.test(text)) {
|
|
55
|
+
return { eligible: true, code: "provider_rate_limited", status };
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
status === 404 &&
|
|
59
|
+
/model|deployment|endpoint|not found|does not exist|unavailable/.test(text)
|
|
60
|
+
) {
|
|
61
|
+
return { eligible: true, code: "provider_model_unavailable", status };
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
status === 408 ||
|
|
65
|
+
status === 503 ||
|
|
66
|
+
status === 529 ||
|
|
67
|
+
status >= 500 ||
|
|
68
|
+
NETWORK_ERROR_CODES.has(code) ||
|
|
69
|
+
/overload|overloaded|capacity|temporarily unavailable|connection refused|network unreachable/.test(text)
|
|
70
|
+
) {
|
|
71
|
+
return { eligible: true, code: "provider_unavailable", status };
|
|
72
|
+
}
|
|
73
|
+
return { eligible: false, code: "provider_request_failed", status };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveProviderHandoff(error, config = {}, { stage = "runtime" } = {}) {
|
|
77
|
+
const sourceProvider = normalizeProviderId(config.provider, "");
|
|
78
|
+
const targetProvider = normalizeProviderId(
|
|
79
|
+
config.providerHandoffProvider || process.env.AGINTI_PROVIDER_HANDOFF_PROVIDER || "localllm",
|
|
80
|
+
""
|
|
81
|
+
);
|
|
82
|
+
if (!enabled(config.allowProviderHandoff ?? process.env.AGINTI_PROVIDER_HANDOFF, true)) return null;
|
|
83
|
+
if (!sourceProvider || sourceProvider === "mock" || sourceProvider === "localllm") return null;
|
|
84
|
+
if (!targetProvider || targetProvider !== "localllm" || targetProvider === sourceProvider) return null;
|
|
85
|
+
if (String(config.routingMode || "smart").trim().toLowerCase() === "manual") return null;
|
|
86
|
+
if (String(config.integrationSessionProfile || "").trim()) return null;
|
|
87
|
+
if (stage !== "preflight" && error?.agintiProviderRequest !== true) return null;
|
|
88
|
+
|
|
89
|
+
const failure = classifyProviderHandoffError(error);
|
|
90
|
+
if (!failure.eligible) return null;
|
|
91
|
+
|
|
92
|
+
const routeModel =
|
|
93
|
+
String(config.providerHandoffRouteModel || process.env.AGINTI_LOCALLLM_ROUTE_MODEL || "").trim() ||
|
|
94
|
+
LOCALLLM_MODEL_TIERS.fast.model;
|
|
95
|
+
const mainModel =
|
|
96
|
+
String(
|
|
97
|
+
config.providerHandoffModel ||
|
|
98
|
+
process.env.AGINTI_PROVIDER_HANDOFF_MODEL ||
|
|
99
|
+
process.env.AGINTI_LOCALLLM_MAIN_MODEL ||
|
|
100
|
+
""
|
|
101
|
+
).trim() || LOCALLLM_MODEL_TIERS.deep.model;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
version: 1,
|
|
105
|
+
sourceProvider,
|
|
106
|
+
sourceModel: String(config.model || ""),
|
|
107
|
+
targetProvider,
|
|
108
|
+
targetModel: mainModel,
|
|
109
|
+
routeModel,
|
|
110
|
+
mainModel,
|
|
111
|
+
spareModel: mainModel,
|
|
112
|
+
failureCode: failure.code,
|
|
113
|
+
status: failure.status,
|
|
114
|
+
runtimePatch: {
|
|
115
|
+
provider: targetProvider,
|
|
116
|
+
model: mainModel,
|
|
117
|
+
routingMode: "manual",
|
|
118
|
+
routeProvider: targetProvider,
|
|
119
|
+
routeModel,
|
|
120
|
+
mainProvider: targetProvider,
|
|
121
|
+
mainModel,
|
|
122
|
+
spareProvider: targetProvider,
|
|
123
|
+
spareModel: mainModel,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|