@framers/agentos 0.12.3 → 0.12.4
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/api/agent.d.ts +7 -5
- package/dist/api/agent.d.ts.map +1 -1
- package/dist/api/agent.js.map +1 -1
- package/dist/api/generateObject.d.ts +5 -5
- package/dist/api/generateText.d.ts +161 -50
- package/dist/api/generateText.d.ts.map +1 -1
- package/dist/api/generateText.js +255 -78
- package/dist/api/generateText.js.map +1 -1
- package/dist/api/index.d.ts +6 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +5 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/runtime/GMIChunkTransformer.d.ts +1 -1
- package/dist/api/runtime/GMIChunkTransformer.d.ts.map +1 -1
- package/dist/api/runtime/GMIChunkTransformer.js +7 -2
- package/dist/api/runtime/GMIChunkTransformer.js.map +1 -1
- package/dist/api/runtime/completionGateway.d.ts +98 -0
- package/dist/api/runtime/completionGateway.d.ts.map +1 -0
- package/dist/api/runtime/completionGateway.js +347 -0
- package/dist/api/runtime/completionGateway.js.map +1 -0
- package/dist/api/runtime/contextWindowFit.d.ts +49 -0
- package/dist/api/runtime/contextWindowFit.d.ts.map +1 -0
- package/dist/api/runtime/contextWindowFit.js +109 -0
- package/dist/api/runtime/contextWindowFit.js.map +1 -0
- package/dist/api/runtime/gatewayProviderManager.d.ts +37 -0
- package/dist/api/runtime/gatewayProviderManager.d.ts.map +1 -0
- package/dist/api/runtime/gatewayProviderManager.js +49 -0
- package/dist/api/runtime/gatewayProviderManager.js.map +1 -0
- package/dist/api/streamText.d.ts.map +1 -1
- package/dist/api/streamText.js +85 -4
- package/dist/api/streamText.js.map +1 -1
- package/dist/cognition/substrate/GMI.d.ts +21 -0
- package/dist/cognition/substrate/GMI.d.ts.map +1 -1
- package/dist/cognition/substrate/GMI.js +286 -88
- package/dist/cognition/substrate/GMI.js.map +1 -1
- package/dist/cognition/substrate/IGMI.d.ts +45 -1
- package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
- package/dist/cognition/substrate/IGMI.js +4 -0
- package/dist/cognition/substrate/IGMI.js.map +1 -1
- package/dist/core/llm/providers/errors/ContextWindowExceededError.d.ts +22 -0
- package/dist/core/llm/providers/errors/ContextWindowExceededError.d.ts.map +1 -0
- package/dist/core/llm/providers/errors/ContextWindowExceededError.js +21 -0
- package/dist/core/llm/providers/errors/ContextWindowExceededError.js.map +1 -0
- package/dist/core/llm/routing/IModelRouter.d.ts +6 -0
- package/dist/core/llm/routing/IModelRouter.d.ts.map +1 -1
- package/dist/core/llm/routing/PolicyAwareRouter.d.ts +7 -0
- package/dist/core/llm/routing/PolicyAwareRouter.d.ts.map +1 -1
- package/dist/core/llm/routing/PolicyAwareRouter.js +12 -0
- package/dist/core/llm/routing/PolicyAwareRouter.js.map +1 -1
- package/dist/core/llm/routing/UncensoredModelCatalog.d.ts +37 -0
- package/dist/core/llm/routing/UncensoredModelCatalog.d.ts.map +1 -1
- package/dist/core/llm/routing/UncensoredModelCatalog.js +64 -74
- package/dist/core/llm/routing/UncensoredModelCatalog.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/api/generateText.js
CHANGED
|
@@ -24,6 +24,9 @@ import { recordAgentOSTurnMetrics, withAgentOSSpan } from '../safety/evaluation/
|
|
|
24
24
|
import { createLogger } from '../core/logging/loggerFactory.js';
|
|
25
25
|
import { globalLLMProviderHealth } from '../core/safety/LLMProviderHealthRegistry.js';
|
|
26
26
|
import { CONTEXT_WINDOW_EXCEEDED_CODE } from '../core/llm/providers/errors/errorCodes.js';
|
|
27
|
+
import { ContextWindowExceededError } from '../core/llm/providers/errors/ContextWindowExceededError.js';
|
|
28
|
+
import { catalogEntryHasCapability, createUncensoredModelCatalog, findCatalogTextModel, } from '../core/llm/routing/UncensoredModelCatalog.js';
|
|
29
|
+
import { checkContextFit } from './runtime/contextWindowFit.js';
|
|
27
30
|
import { describeResponseFormatShape } from './runtime/responseFormatForProvider.js';
|
|
28
31
|
const fallbackLogger = createLogger('fallback');
|
|
29
32
|
/**
|
|
@@ -324,7 +327,7 @@ export function addModelUsage(target, usage) {
|
|
|
324
327
|
* The usage a provider error reports for the request it ended, such as the
|
|
325
328
|
* billed tokens of a refused turn (`details.usage`).
|
|
326
329
|
*
|
|
327
|
-
* @internal Shared with streamText.
|
|
330
|
+
* @internal Shared with streamText and the completion gateway.
|
|
328
331
|
*/
|
|
329
332
|
export function usageOfError(error) {
|
|
330
333
|
return error?.details?.usage;
|
|
@@ -662,81 +665,182 @@ export function fallbackHopOverrides(opts, entry) {
|
|
|
662
665
|
};
|
|
663
666
|
}
|
|
664
667
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
668
|
+
* The policy tier a call's fallback chain is built for: the explicit route
|
|
669
|
+
* tier, the call's tier, the host policy's tier (`standard` when a host
|
|
670
|
+
* policy names none), then the router's default. The primary's route block
|
|
671
|
+
* sends the first three terms only, so a delegated base router still sees no
|
|
672
|
+
* tier when no explicit source set one.
|
|
673
|
+
*/
|
|
674
|
+
export function resolvePolicyTier(opts) {
|
|
675
|
+
return (opts.routerParams?.policyTier ??
|
|
676
|
+
opts.policyTier ??
|
|
677
|
+
hostPolicyToRouteParams(opts.hostPolicy).policyTier ??
|
|
678
|
+
opts.router?.policyTier);
|
|
679
|
+
}
|
|
680
|
+
/** @internal */
|
|
681
|
+
export const INITIAL_FALLBACK_WALK = Object.freeze({
|
|
682
|
+
refillsOwed: 0,
|
|
683
|
+
refusalSeen: false,
|
|
684
|
+
});
|
|
685
|
+
/** Claude, direct or through OpenRouter. */
|
|
686
|
+
function isClaudeEntry(entry) {
|
|
687
|
+
return (entry.provider === 'anthropic' ||
|
|
688
|
+
(entry.provider === 'openrouter' && (entry.model ?? '').startsWith('anthropic/')));
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* The capabilities a call names explicitly (host policy and route params).
|
|
692
|
+
* Native tool calling is not inferred from the presence of tools: a model
|
|
693
|
+
* without it serves a tool-carrying call through the prompt shim.
|
|
670
694
|
*
|
|
671
|
-
*
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
695
|
+
* @internal
|
|
696
|
+
*/
|
|
697
|
+
export function explicitRequiredCapabilities(opts) {
|
|
698
|
+
return (mergeRequiredCapabilities(hostPolicyToRouteParams(opts.hostPolicy).requiredCapabilities, opts.routerParams?.requiredCapabilities) ?? []);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* The provider and model a fallback entry is sent as: resolved the way the
|
|
702
|
+
* leg's own call resolves it (a provider's default model when the entry
|
|
703
|
+
* names none, a `provider:model` id split), or the entry as written when it
|
|
704
|
+
* does not resolve.
|
|
705
|
+
*/
|
|
706
|
+
function fallbackEntrySentAs(entry) {
|
|
707
|
+
try {
|
|
708
|
+
const { providerId, modelId } = resolveModelOption({ provider: entry.provider, model: entry.model }, 'text');
|
|
709
|
+
return { provider: providerId, model: modelId };
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return { provider: entry.provider, model: entry.model };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Resolve a call's fallback chain once, at the start of the top-level walk.
|
|
717
|
+
* Policy-chain entries (`origin: 'policy-default'`) naming the failed first
|
|
718
|
+
* model are dropped; every entry is checked, as the provider and model it
|
|
719
|
+
* will be sent as, against the call's explicitly required capabilities and
|
|
720
|
+
* excluded models (a model outside the catalog counts as capable); the
|
|
721
|
+
* uncensored group's first two remaining entries become standing legs and
|
|
722
|
+
* the rest refills. Caller-written entries keep their order.
|
|
688
723
|
*
|
|
689
|
-
*
|
|
690
|
-
|
|
691
|
-
|
|
724
|
+
* @internal
|
|
725
|
+
*/
|
|
726
|
+
export function resolveFallbackChain(chain, ctx) {
|
|
727
|
+
const required = ctx.requiredCapabilities ?? [];
|
|
728
|
+
const excluded = new Set(ctx.excludedModelIds ?? []);
|
|
729
|
+
const resolved = [];
|
|
730
|
+
let uncensoredLegs = 0;
|
|
731
|
+
for (const entry of chain) {
|
|
732
|
+
if (entry.origin === 'policy-default' &&
|
|
733
|
+
entry.provider === ctx.primary.provider &&
|
|
734
|
+
entry.model === ctx.primary.model) {
|
|
735
|
+
ctx.onSkip?.(entry, 'failed_primary');
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const sentAs = fallbackEntrySentAs(entry);
|
|
739
|
+
if (sentAs.model !== undefined && excluded.has(sentAs.model)) {
|
|
740
|
+
ctx.onSkip?.(entry, 'excluded_model');
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const catalogEntry = sentAs.model !== undefined ? findCatalogTextModel(sentAs.model, sentAs.provider) : undefined;
|
|
744
|
+
if (catalogEntry && required.some((capability) => !catalogEntryHasCapability(catalogEntry, capability))) {
|
|
745
|
+
ctx.onSkip?.(entry, 'missing_capability');
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (entry.origin === 'policy-default' && entry.group === 'uncensored') {
|
|
749
|
+
resolved.push({ ...entry, walkRole: uncensoredLegs < 2 ? 'standing' : 'refill' });
|
|
750
|
+
uncensoredLegs += 1;
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
resolved.push(entry);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return resolved;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Whether the walk runs `entry` now: a refill only while a standing leg is
|
|
760
|
+
* owed one, and none of the policy chain's Claude legs after a refusal.
|
|
692
761
|
*
|
|
693
|
-
*
|
|
694
|
-
|
|
695
|
-
|
|
762
|
+
* @internal
|
|
763
|
+
*/
|
|
764
|
+
export function gateFallbackEntry(state, entry) {
|
|
765
|
+
if (state.refusalSeen && entry.origin === 'policy-default' && isClaudeEntry(entry)) {
|
|
766
|
+
return { run: false, reason: 'claude_after_refusal' };
|
|
767
|
+
}
|
|
768
|
+
if (entry.walkRole === 'refill') {
|
|
769
|
+
if (state.refillsOwed < 1)
|
|
770
|
+
return { run: false, reason: 'refill_not_owed' };
|
|
771
|
+
return { run: true, state: { ...state, refillsOwed: state.refillsOwed - 1 } };
|
|
772
|
+
}
|
|
773
|
+
return { run: true, state };
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* The walk's state after an attempt failed with `error`: a standing leg that
|
|
777
|
+
* failed for anything but a content decline (a timeout, an open breaker and
|
|
778
|
+
* the context check's refusal included) is owed a refill, and a model
|
|
779
|
+
* refusal (code `content_filter`; a filter's `content_policy_violation`
|
|
780
|
+
* does not count) is recorded for the rest of the walk.
|
|
781
|
+
*
|
|
782
|
+
* @internal
|
|
783
|
+
*/
|
|
784
|
+
export function advanceFallbackWalk(state, role, error) {
|
|
785
|
+
const owed = role === 'standing' && !isContentPolicyRefusal(error) ? 1 : 0;
|
|
786
|
+
const refusal = error?.code === 'content_filter';
|
|
787
|
+
return {
|
|
788
|
+
refillsOwed: state.refillsOwed + owed,
|
|
789
|
+
refusalSeen: state.refusalSeen || refusal,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/** The catalog the policy chain reads its ladders from. */
|
|
793
|
+
const POLICY_CATALOG = createUncensoredModelCatalog();
|
|
794
|
+
/** A catalog model never used as a fallback leg: too weak for a rescue reply. */
|
|
795
|
+
const NEVER_A_LEG_MODEL = 'meta-llama/llama-3.1-8b-instruct';
|
|
796
|
+
/**
|
|
797
|
+
* Build a policy-tier-aware fallback chain, for callers that pass
|
|
798
|
+
* `policyTier: 'mature' | 'private-adult'`: a refusal or an outage of the
|
|
799
|
+
* primary walks to an uncensored model before the availability legs.
|
|
800
|
+
*
|
|
801
|
+
* Mature and private-adult: the tier's catalog ladder
|
|
802
|
+
* (the catalog's `getFallbackLadder`; private-adult keeps the
|
|
803
|
+
* models that permit `erotic`) as OpenRouter legs in the `uncensored`
|
|
804
|
+
* group, llama-3.1-8b left out, then the {@link buildFallbackChain} suffix.
|
|
805
|
+
* Every entry is tagged `origin: 'policy-default'`, which lets the walk drop
|
|
806
|
+
* the leg naming the failed first model, keep two standing uncensored legs,
|
|
807
|
+
* and pass over Claude legs after a refusal. Safe, standard and an absent
|
|
808
|
+
* tier get {@link buildFallbackChain} unchanged and untagged.
|
|
696
809
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* @param
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
810
|
+
* Each leg pins `cache: false`. A missing key drops its legs instead of
|
|
811
|
+
* throwing, so a partial deploy still gets a shorter usable chain.
|
|
812
|
+
*
|
|
813
|
+
* @param tier - The call's content tier.
|
|
814
|
+
* @param excludeProvider - Provider to leave out of the availability suffix
|
|
815
|
+
* (typically the failed primary's). The ladder legs ignore it: another
|
|
816
|
+
* model on the same provider is a valid rescue, and the walk drops the
|
|
817
|
+
* exact failed model.
|
|
704
818
|
*/
|
|
705
819
|
export function buildPolicyAwareFallbackChain(tier, excludeProvider) {
|
|
706
|
-
|
|
707
|
-
if (!isMatureTier) {
|
|
820
|
+
if (tier !== 'mature' && tier !== 'private-adult') {
|
|
708
821
|
return buildFallbackChain(excludeProvider);
|
|
709
822
|
}
|
|
710
823
|
const chain = [];
|
|
711
|
-
// Hermes 3 405B leads: uncensored, large, instruction-following
|
|
712
|
-
// proven on the wilds-ai identity-generation path. Skipped when
|
|
713
|
-
// OPENROUTER_API_KEY is absent rather than throwing; the suffix
|
|
714
|
-
// chain may still produce a usable fallback.
|
|
715
824
|
if (process.env.OPENROUTER_API_KEY) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
cache: false,
|
|
731
|
-
});
|
|
825
|
+
const ladder = tier === 'private-adult'
|
|
826
|
+
? POLICY_CATALOG.getFallbackLadder('private-adult', { contentIntent: 'erotic' })
|
|
827
|
+
: POLICY_CATALOG.getFallbackLadder('mature');
|
|
828
|
+
for (const entry of ladder) {
|
|
829
|
+
if (entry.modelId === NEVER_A_LEG_MODEL)
|
|
830
|
+
continue;
|
|
831
|
+
chain.push({
|
|
832
|
+
provider: entry.providerId,
|
|
833
|
+
model: entry.modelId,
|
|
834
|
+
cache: false,
|
|
835
|
+
origin: 'policy-default',
|
|
836
|
+
group: 'uncensored',
|
|
837
|
+
});
|
|
838
|
+
}
|
|
732
839
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const alreadyInChain = chain.some((existing) => existing.provider === entry.provider && existing.model === entry.model);
|
|
738
|
-
if (!alreadyInChain)
|
|
739
|
-
chain.push(entry);
|
|
840
|
+
for (const entry of buildFallbackChain(excludeProvider)) {
|
|
841
|
+
const listed = chain.some((e) => e.provider === entry.provider && e.model === entry.model);
|
|
842
|
+
if (!listed)
|
|
843
|
+
chain.push({ ...entry, origin: 'policy-default' });
|
|
740
844
|
}
|
|
741
845
|
return chain;
|
|
742
846
|
}
|
|
@@ -947,6 +1051,28 @@ export async function generateText(opts) {
|
|
|
947
1051
|
function: { name: t.name, description: t.description, parameters: t.inputSchema },
|
|
948
1052
|
}))
|
|
949
1053
|
: undefined;
|
|
1054
|
+
// A catalog model is sent only a request it can hold: otherwise the
|
|
1055
|
+
// call throws in place of the send and the walk moves on. Checked on
|
|
1056
|
+
// the first native send and on the prompt shim's first send.
|
|
1057
|
+
const assertFitsContextWindow = (sent) => {
|
|
1058
|
+
const fit = checkContextFit({
|
|
1059
|
+
provider: resolved.providerId,
|
|
1060
|
+
model: resolved.modelId,
|
|
1061
|
+
messages: sent.messages,
|
|
1062
|
+
tools: sent.tools,
|
|
1063
|
+
maxTokens: opts.maxTokens,
|
|
1064
|
+
customModelParams: opts.customModelParams,
|
|
1065
|
+
});
|
|
1066
|
+
if (!fit.fits && fit.contextWindow !== undefined) {
|
|
1067
|
+
throw new ContextWindowExceededError({
|
|
1068
|
+
provider: resolved.providerId,
|
|
1069
|
+
model: resolved.modelId,
|
|
1070
|
+
contextWindow: fit.contextWindow,
|
|
1071
|
+
estimatedInputTokens: fit.estimatedInputTokens,
|
|
1072
|
+
outputTokens: fit.outputTokens,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
950
1076
|
const allToolCalls = [];
|
|
951
1077
|
const totalUsage = attemptUsage;
|
|
952
1078
|
// Provider-reported model id of the final step (spec batch-1 C1);
|
|
@@ -1003,6 +1129,7 @@ export async function generateText(opts) {
|
|
|
1003
1129
|
// provider's tool-unsupported error (see the catch after the loop).
|
|
1004
1130
|
const toolMode = opts.toolMode ?? 'auto';
|
|
1005
1131
|
const shimMaxRoundtrips = opts.maxSteps ?? 5;
|
|
1132
|
+
let shimSendChecked = false;
|
|
1006
1133
|
const runShim = async () => {
|
|
1007
1134
|
const loopResult = await runEmulatedToolLoop({
|
|
1008
1135
|
tools: Array.from(toolMap.values()),
|
|
@@ -1018,6 +1145,12 @@ export async function generateText(opts) {
|
|
|
1018
1145
|
messages: toShimMessages(messages),
|
|
1019
1146
|
maxRoundtrips: shimMaxRoundtrips,
|
|
1020
1147
|
callModel: async (msgs) => {
|
|
1148
|
+
// The shim sends rendered tool text in place of native schemas,
|
|
1149
|
+
// so its first send is checked on its own.
|
|
1150
|
+
if (!shimSendChecked) {
|
|
1151
|
+
shimSendChecked = true;
|
|
1152
|
+
assertFitsContextWindow({ messages: msgs });
|
|
1153
|
+
}
|
|
1021
1154
|
const r = await provider.generateCompletion(resolved.modelId, msgs, {
|
|
1022
1155
|
temperature: opts.temperature,
|
|
1023
1156
|
...(opts.topP !== undefined ? { topP: opts.topP } : {}),
|
|
@@ -1176,6 +1309,10 @@ export async function generateText(opts) {
|
|
|
1176
1309
|
console.warn('[agentos] onBeforeGeneration hook error:', hookErr);
|
|
1177
1310
|
}
|
|
1178
1311
|
}
|
|
1312
|
+
// A continuation leg's first send carries its completed tool rounds.
|
|
1313
|
+
// Later steps are not checked again.
|
|
1314
|
+
if (step === 0)
|
|
1315
|
+
assertFitsContextWindow({ messages: effectiveMessages, tools: toolSchemas });
|
|
1179
1316
|
const response = await withAgentOSSpan('agentos.api.generate_text.step', async (stepSpan) => {
|
|
1180
1317
|
stepSpan?.setAttribute('llm.provider', resolved.providerId);
|
|
1181
1318
|
stepSpan?.setAttribute('llm.model', resolved.modelId);
|
|
@@ -1668,25 +1805,55 @@ export async function generateText(opts) {
|
|
|
1668
1805
|
{ provider: primaryProviderId ?? 'unknown', model: primaryModelId, ok: false },
|
|
1669
1806
|
];
|
|
1670
1807
|
// ── Fallback chain ────────────────────────────────────────────────
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1673
|
-
//
|
|
1674
|
-
// the policy
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1808
|
+
// Caller-supplied wins, undefined auto-builds from env keys for the
|
|
1809
|
+
// call's resolved tier (the policy-aware chain on mature and
|
|
1810
|
+
// private-adult), an empty array opts out. The top-level walk resolves
|
|
1811
|
+
// the chain once: it drops the policy chain's entry for the failed first
|
|
1812
|
+
// model, applies the call's explicit capability and exclusion
|
|
1813
|
+
// requirements, and marks the uncensored group's standing legs and
|
|
1814
|
+
// refills. A leg receives its resolved slice and never resolves it again.
|
|
1815
|
+
const walkTier = resolvePolicyTier(opts);
|
|
1816
|
+
const chainEntries = opts.fallbackProviders === undefined
|
|
1817
|
+
? buildPolicyAwareFallbackChain(walkTier, metricProviderId)
|
|
1680
1818
|
: opts.fallbackProviders;
|
|
1819
|
+
const logLegSkip = (entry, reason) => fallbackLogger.info('provider fallback skipped', {
|
|
1820
|
+
event: 'fallback_leg_skipped',
|
|
1821
|
+
api: 'generateText',
|
|
1822
|
+
reason,
|
|
1823
|
+
primaryProvider: metricProviderId,
|
|
1824
|
+
fallbackProvider: entry.provider,
|
|
1825
|
+
fallbackModel: entry.model,
|
|
1826
|
+
});
|
|
1827
|
+
// Resolved only when the walk runs, so its skip lines describe a walk.
|
|
1681
1828
|
// A call whose prompt-shim tools already ran cannot be continued, and a
|
|
1682
1829
|
// restart would run them again; it surfaces the error instead.
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1830
|
+
const walks = isRetryableError(error) && !toolProgress.shimRanTool;
|
|
1831
|
+
const effectiveFallbacks = !walks
|
|
1832
|
+
? []
|
|
1833
|
+
: opts.__fallbackWalk
|
|
1834
|
+
? chainEntries
|
|
1835
|
+
: resolveFallbackChain(chainEntries, {
|
|
1836
|
+
primary: { provider: metricProviderId, model: metricModelId },
|
|
1837
|
+
requiredCapabilities: explicitRequiredCapabilities(opts),
|
|
1838
|
+
excludedModelIds: opts.routerParams?.excludedModelIds,
|
|
1839
|
+
onSkip: logLegSkip,
|
|
1840
|
+
});
|
|
1841
|
+
// This attempt's own failure updates the walk: a refusal is recorded,
|
|
1842
|
+
// and a standing leg that failed on availability is owed a refill.
|
|
1843
|
+
let walkState = advanceFallbackWalk(opts.__fallbackWalk?.state ?? INITIAL_FALLBACK_WALK, opts.__fallbackWalk?.role, error);
|
|
1844
|
+
if (walks && effectiveFallbacks.length) {
|
|
1686
1845
|
let lastError = error;
|
|
1687
1846
|
let attempt = 0;
|
|
1688
1847
|
for (const fb of effectiveFallbacks) {
|
|
1689
1848
|
attempt += 1;
|
|
1849
|
+
// A refill runs only while a standing leg is owed one, and the
|
|
1850
|
+
// policy chain's Claude legs are passed over after a refusal.
|
|
1851
|
+
const gate = gateFallbackEntry(walkState, fb);
|
|
1852
|
+
if (!gate.run) {
|
|
1853
|
+
logLegSkip(fb, gate.reason);
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
walkState = gate.state;
|
|
1690
1857
|
// Skip fallback entries whose breaker is already open. Without
|
|
1691
1858
|
// this check, the loop would still spend a full network round-
|
|
1692
1859
|
// trip on every dead fallback in the chain before reaching the
|
|
@@ -1702,6 +1869,9 @@ export async function generateText(opts) {
|
|
|
1702
1869
|
fallbackModel: fb.model,
|
|
1703
1870
|
attempt,
|
|
1704
1871
|
});
|
|
1872
|
+
// An open breaker is an availability failure: a standing leg is
|
|
1873
|
+
// owed a refill.
|
|
1874
|
+
walkState = advanceFallbackWalk(walkState, fb.walkRole, undefined);
|
|
1705
1875
|
continue;
|
|
1706
1876
|
}
|
|
1707
1877
|
try {
|
|
@@ -1735,6 +1905,12 @@ export async function generateText(opts) {
|
|
|
1735
1905
|
...opts,
|
|
1736
1906
|
provider: fb.provider,
|
|
1737
1907
|
model: fb.model,
|
|
1908
|
+
// Legs run as named: no router re-picks the model, and the call's
|
|
1909
|
+
// resolved tier travels with them.
|
|
1910
|
+
router: undefined,
|
|
1911
|
+
policyTier: walkTier,
|
|
1912
|
+
// The walk's state before this leg, and its role.
|
|
1913
|
+
__fallbackWalk: { state: walkState, role: fb.walkRole },
|
|
1738
1914
|
// When a builder exists, ALWAYS override _responseFormat —
|
|
1739
1915
|
// including with an explicit `undefined` on builder failure or
|
|
1740
1916
|
// decline. Merely omitting the key would let the `...opts`
|
|
@@ -1764,7 +1940,7 @@ export async function generateText(opts) {
|
|
|
1764
1940
|
// fallback provider rather than the primary's overrides.
|
|
1765
1941
|
apiKey: undefined,
|
|
1766
1942
|
baseUrl: undefined,
|
|
1767
|
-
// Preserve the REMAINING
|
|
1943
|
+
// Preserve the REMAINING resolved chain (entries AFTER the current
|
|
1768
1944
|
// fb; `attempt` is 1-indexed so slice(attempt) drops fb and all
|
|
1769
1945
|
// already-tried entries). This stops the recursion from rebuilding
|
|
1770
1946
|
// the default cheap chain (which includes gpt-4o-mini) when a
|
|
@@ -1844,6 +2020,7 @@ export async function generateText(opts) {
|
|
|
1844
2020
|
// The leg walked every entry after it and all of them failed.
|
|
1845
2021
|
if (chainWalkedBefore(fbError))
|
|
1846
2022
|
break;
|
|
2023
|
+
walkState = advanceFallbackWalk(walkState, fb.walkRole, fbError);
|
|
1847
2024
|
}
|
|
1848
2025
|
}
|
|
1849
2026
|
// All fallbacks exhausted: fall through to throw
|