@bitkyc08/opencodex 2.74.0 → 2.75.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/gui/dist/assets/{App-KLz1TAXi.css → App-ia4U0ufY.css} +1 -1
- package/gui/dist/assets/{App-B00Yq11u.js → App-xm0zGNOe.js} +3 -3
- package/gui/dist/assets/{Tray-D3Ij1Cvf.js → Tray-CGAD2AGk.js} +1 -1
- package/gui/dist/assets/{index-BYQQfBGK.js → index-D_RGAT6Q.js} +2 -2
- package/gui/dist/assets/{quota-summary-yPtcEhGI.js → quota-summary-CUFq48t-.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/devin/cloud-direct/chat.ts +38 -6
- package/src/adapters/devin/live-models.ts +28 -8
- package/src/adapters/devin.ts +1 -1
- package/src/claude/devin-output-order.ts +98 -0
- package/src/codex/auth-api/pool-quota-probe.ts +2 -1
- package/src/codex/catalog/retained-sync.ts +9 -0
- package/src/codex/inject/config-toml.ts +179 -248
- package/src/codex/inject/provider-table.ts +5 -2
- package/src/codex/inject/remove.ts +23 -8
- package/src/codex/injected-marker.ts +28 -23
- package/src/codex/quota-auto-refresh.ts +1 -1
- package/src/codex/quota.ts +12 -4
- package/src/codex/toml-source-lines.ts +93 -1
- package/src/config/live-reconcile.ts +18 -0
- package/src/config/rebase-provenance.ts +12 -0
- package/src/config.ts +4 -3
- package/src/generated/compatibility-version.json +50 -38
- package/src/images/loop.ts +2 -0
- package/src/integrations/cursor-local-installer.ts +29 -2
- package/src/lib/redact.ts +54 -8
- package/src/oauth/anthropic-account-refusal.ts +76 -0
- package/src/oauth/anthropic-routing.ts +38 -10
- package/src/providers/new-model-policy-runtime.ts +83 -0
- package/src/providers/new-model-policy.ts +25 -0
- package/src/providers/registry/entries-core.ts +3 -0
- package/src/providers/registry/model-seeds.ts +31 -2
- package/src/providers/xai-transport.ts +2 -2
- package/src/server/claude-messages.ts +33 -10
- package/src/server/management/agent-settings-routes.ts +3 -1
- package/src/server/management/model-rows.ts +34 -4
- package/src/server/management/shared.ts +18 -2
- package/src/server/responses/adapter-continuation.ts +13 -21
- package/src/server/responses/adapter-delivery.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +10 -21
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +2 -1
- package/src/server/responses/request-transport.ts +2 -0
- package/src/server/responses/run-turn-execution.ts +25 -5
- package/src/server/responses/sidecar-execution.ts +9 -14
- package/src/web-search/loop.ts +7 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Default OFF. When enabled:
|
|
5
5
|
* - Sticky session affinity across requests that share a session key
|
|
6
|
-
* - 429
|
|
6
|
+
* - 429 and classified pre-output account 403s cool the failed account and fail over
|
|
7
7
|
* - New sessions use `strategy` (default quota): lowest known fiveHour usage (#493),
|
|
8
8
|
* round-robin, or fill-first — affinity still wins for bound sessions
|
|
9
9
|
*
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Affinity is process-local (lost on restart). Cooldown uses Retry-After when present, else
|
|
14
14
|
* the reset time of whichever rate-limit window upstream reports as rejected, else a default
|
|
15
|
-
* backoff.
|
|
16
|
-
*
|
|
15
|
+
* backoff. Classified entitlement/billing 403s use a ten-minute default cooldown.
|
|
16
|
+
* Token refresh failures retain the existing store needsReauth policy.
|
|
17
17
|
*/
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { captureOAuthAccountSelection, commitOAuthAccountSelection, credentialGeneration, getAccountSet, getAccountCredential, getAccountCredentialWithStatus } from "./store";
|
|
@@ -53,6 +53,7 @@ export type AnthropicRateLimitHeaders = Pick<Headers, "get">;
|
|
|
53
53
|
const PROVIDER = "anthropic";
|
|
54
54
|
/** Backoff only when upstream supplies no usable deadline. */
|
|
55
55
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
56
|
+
const ACCOUNT_REFUSAL_COOLDOWN_MS = 10 * 60_000;
|
|
56
57
|
const AFFINITY_IDLE_TTL_MS = 24 * 60 * 60_000;
|
|
57
58
|
const MAX_AFFINITY_ENTRIES = 2_000;
|
|
58
59
|
const MAX_AFFINITY_COMPONENT_BYTES = 512;
|
|
@@ -879,7 +880,21 @@ export function rotateAnthropicAccountOn429(
|
|
|
879
880
|
rateLimitHeaders?: AnthropicRateLimitHeaders | null,
|
|
880
881
|
decision: AnthropicRouteDecision | null = null,
|
|
881
882
|
): string | null {
|
|
882
|
-
|
|
883
|
+
return rotateAnthropicAccountOnRefusal(config, failedAccountId, 429, retryAfterHeader, sessionKey, now, rateLimitHeaders, decision);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** The caller proves that a 403 is an account refusal before entering this pool policy. */
|
|
887
|
+
export function rotateAnthropicAccountOnRefusal(
|
|
888
|
+
config: OcxConfig,
|
|
889
|
+
failedAccountId: string,
|
|
890
|
+
status: 429 | 403,
|
|
891
|
+
retryAfterHeader: string | null | undefined,
|
|
892
|
+
sessionKey?: string | null,
|
|
893
|
+
now = Date.now(),
|
|
894
|
+
rateLimitHeaders?: AnthropicRateLimitHeaders | null,
|
|
895
|
+
decision: AnthropicRouteDecision | null = null,
|
|
896
|
+
): string | null {
|
|
897
|
+
if (!recordAnthropicAccountRefusal(config, failedAccountId, status, retryAfterHeader, now, rateLimitHeaders)) return null;
|
|
883
898
|
|
|
884
899
|
// The pool's strategy is a PROACTIVE policy. When the pool is disabled, reactive
|
|
885
900
|
// presence-only recovery must not silently reactivate round-robin/fill-first merely
|
|
@@ -889,13 +904,14 @@ export function rotateAnthropicAccountOn429(
|
|
|
889
904
|
? pickAlternateAnthropicAccount(config, failedAccountId, now, decision)
|
|
890
905
|
: pickLowestUsage(config, failedAccountId, now);
|
|
891
906
|
if (!next) {
|
|
892
|
-
console.warn(`[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}no eligible replacement; returning
|
|
907
|
+
console.warn(`[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}no eligible replacement; returning ${status}`);
|
|
893
908
|
return null;
|
|
894
909
|
}
|
|
895
910
|
|
|
896
|
-
console.warn(
|
|
897
|
-
`[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}
|
|
898
|
-
|
|
911
|
+
console.warn(status === 403
|
|
912
|
+
? `[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}account unavailable (403); failing over`
|
|
913
|
+
: `[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}429 on ${formatAnthropicAccountOrdinal(failedAccountId)}; failing over to ${formatAnthropicAccountOrdinal(next)}`);
|
|
914
|
+
|
|
899
915
|
return next;
|
|
900
916
|
}
|
|
901
917
|
|
|
@@ -906,6 +922,18 @@ export function recordAnthropicAccount429(
|
|
|
906
922
|
retryAfterHeader: string | null | undefined,
|
|
907
923
|
now = Date.now(),
|
|
908
924
|
rateLimitHeaders?: AnthropicRateLimitHeaders | null,
|
|
925
|
+
): boolean {
|
|
926
|
+
return recordAnthropicAccountRefusal(config, failedAccountId, 429, retryAfterHeader, now, rateLimitHeaders);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Account entitlement refusals use a finite backoff; renewing a plan does not require login. */
|
|
930
|
+
export function recordAnthropicAccountRefusal(
|
|
931
|
+
config: OcxConfig,
|
|
932
|
+
failedAccountId: string,
|
|
933
|
+
status: 429 | 403,
|
|
934
|
+
retryAfterHeader: string | null | undefined,
|
|
935
|
+
now = Date.now(),
|
|
936
|
+
rateLimitHeaders?: AnthropicRateLimitHeaders | null,
|
|
909
937
|
): boolean {
|
|
910
938
|
// Reactive 429 failover is NOT gated on the pool flag. That flag buys PROACTIVE routing --
|
|
911
939
|
// session affinity, quota-ranked new-session selection, autoSwitchThreshold, strategy -- all
|
|
@@ -923,8 +951,8 @@ export function recordAnthropicAccount429(
|
|
|
923
951
|
// without that fallback such a refusal cools for the 60s default and the exhausted
|
|
924
952
|
// account is back in the rotation a minute later.
|
|
925
953
|
const parsedRetry = parseRetryAfterMs(retryAfterHeader, now);
|
|
926
|
-
const resetDerived = parsedRetry === undefined ? parseRateLimitReset(rateLimitHeaders, now) : undefined;
|
|
927
|
-
const cooldownMs = parsedRetry ?? resetDerived?.delayMs ?? DEFAULT_COOLDOWN_MS;
|
|
954
|
+
const resetDerived = status === 429 && parsedRetry === undefined ? parseRateLimitReset(rateLimitHeaders, now) : undefined;
|
|
955
|
+
const cooldownMs = parsedRetry ?? resetDerived?.delayMs ?? (status === 403 ? ACCOUNT_REFUSAL_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
|
|
928
956
|
const cooldownGeneration = noteAnthropicCooldownMutation(failedAccountId);
|
|
929
957
|
upstreamHealth.set(failedAccountId, {
|
|
930
958
|
cooldownUntil: now + cooldownMs,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { mutatePersistedConfig, readConfigDiagnostics, validateConfigCandidate } from "../config";
|
|
3
|
+
import { fileBackedConfigPath } from "../config/rebase-provenance";
|
|
4
|
+
import { getConfigPath } from "../config/paths";
|
|
5
|
+
import { observeModelCacheRevision } from "../codex/model-cache";
|
|
6
|
+
import { adoptPersistedModelDiscovery } from "../config/live-reconcile";
|
|
7
|
+
import type { OcxConfig } from "../types";
|
|
8
|
+
import { reconcileSuccessfulModelDiscoveries } from "./new-model-policy";
|
|
9
|
+
|
|
10
|
+
function inventory(config: OcxConfig): unknown {
|
|
11
|
+
const validated = validateConfigCandidate(config);
|
|
12
|
+
if (!validated.ok) return null;
|
|
13
|
+
const providers = Object.fromEntries(Object.entries(validated.config.providers).map(([name, provider]) => [name, {
|
|
14
|
+
...provider,
|
|
15
|
+
// Initial selection may finish during this discovery, but a re-registration is distinct.
|
|
16
|
+
initialModelSelection: provider.initialModelSelection?.registrationId,
|
|
17
|
+
}]));
|
|
18
|
+
// Compare normalized persisted data, not injected fetch functions. Never log this identity.
|
|
19
|
+
return JSON.parse(JSON.stringify({ providers, combos: validated.config.combos,
|
|
20
|
+
policy: validated.config.modelDiscovery?.newModelPolicy }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function captureModelDiscoveryBaseline(config: OcxConfig) {
|
|
24
|
+
const identity = inventory(config);
|
|
25
|
+
const disk = readConfigDiagnostics();
|
|
26
|
+
return { identity, persisted: identity !== null && disk.source === "file"
|
|
27
|
+
&& isDeepStrictEqual(identity, inventory(disk.config)) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Publish new-arrival choices before returning discovery rows to any visibility consumer. */
|
|
31
|
+
export function finalizeModelDiscovery(
|
|
32
|
+
config: OcxConfig,
|
|
33
|
+
baseline: ReturnType<typeof captureModelDiscoveryBaseline>,
|
|
34
|
+
models: Iterable<{ provider: string; id: string; custom?: boolean }>,
|
|
35
|
+
authoritativeProviders: string[],
|
|
36
|
+
revisions: Map<string, string>,
|
|
37
|
+
/** Read consumers may publish detached policy state; writers require persisted decisions. */
|
|
38
|
+
publishProjection?: (projection: OcxConfig) => void,
|
|
39
|
+
): boolean {
|
|
40
|
+
const filePath = fileBackedConfigPath(config);
|
|
41
|
+
const rows = [...models];
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const current = () => (filePath === undefined || filePath === getConfigPath())
|
|
44
|
+
&& isDeepStrictEqual(baseline.identity, inventory(config))
|
|
45
|
+
&& [...revisions].every(([provider, revision]) => observeModelCacheRevision(provider) === revision);
|
|
46
|
+
if (!current()) return false;
|
|
47
|
+
const reconcile = (target: OcxConfig) => reconcileSuccessfulModelDiscoveries({
|
|
48
|
+
config: target, models: rows, authoritativeProviders, now, mode: "discovery",
|
|
49
|
+
});
|
|
50
|
+
if (!baseline.persisted && filePath === undefined) {
|
|
51
|
+
// Standalone/synthetic callers may project their own config, never overwrite another home.
|
|
52
|
+
reconcile(config);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!baseline.persisted && publishProjection) {
|
|
56
|
+
// Drift already existed at admission. Do not add unsaved disables or advance the live
|
|
57
|
+
// merge baseline: a later rebase-save must preserve the operator's on-disk choices.
|
|
58
|
+
const projection = { ...config, modelDiscovery: structuredClone(config.modelDiscovery),
|
|
59
|
+
disabledModels: config.disabledModels === undefined ? undefined : [...config.disabledModels] };
|
|
60
|
+
reconcile(projection);
|
|
61
|
+
publishProjection(projection);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// A file-loaded instance remains file-backed after inventory drift. The locked
|
|
65
|
+
// freshness gate below refuses stale rows without creating unsaved live disables.
|
|
66
|
+
try {
|
|
67
|
+
const result = mutatePersistedConfig(fresh => {
|
|
68
|
+
if (!current() || !isDeepStrictEqual(baseline.identity, inventory(fresh))) {
|
|
69
|
+
return { changed: false, value: null };
|
|
70
|
+
}
|
|
71
|
+
// Re-evaluate against the latest baseline and manual choices under the mutation lock.
|
|
72
|
+
// A second caller then adopts a first caller's result rather than disabling an ID twice.
|
|
73
|
+
const changed = reconcile(fresh);
|
|
74
|
+
return { changed, value: { modelDiscovery: fresh.modelDiscovery, disabledModels: fresh.disabledModels } };
|
|
75
|
+
});
|
|
76
|
+
if (result.status === "unavailable" || !result.value) return false;
|
|
77
|
+
adoptPersistedModelDiscovery(config, result.value);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
// Do not publish an unrecorded arrival when persistence failed. The caller retries discovery.
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -8,6 +8,17 @@ export const MAX_RECENT_ARRIVALS_PER_PROVIDER = 50;
|
|
|
8
8
|
export type KnownModelBaseline = NonNullable<NonNullable<OcxConfig["modelDiscovery"]>["knownModels"]>[string];
|
|
9
9
|
export type NewModelPolicy = "on" | "off";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Which side of the reconciliation is running.
|
|
13
|
+
*
|
|
14
|
+
* `converge` (the default) is the catalog convergence cycle: it advances the removal grace and
|
|
15
|
+
* retires ids missing for {@link MODEL_REMOVAL_GRACE_FETCHES} cycles. `discovery` is the HTTP read
|
|
16
|
+
* side (GET /v1/models), which is polled far more often than convergence runs; it absorbs genuine
|
|
17
|
+
* arrivals and reappearances but never advances removal accounting, so a transiently absent id
|
|
18
|
+
* cannot be retired by repeated polls.
|
|
19
|
+
*/
|
|
20
|
+
export type NewModelPolicyMode = "converge" | "discovery";
|
|
21
|
+
|
|
11
22
|
export interface NewModelPolicyResult {
|
|
12
23
|
newIds: string[];
|
|
13
24
|
nextBaseline: KnownModelBaseline;
|
|
@@ -44,6 +55,8 @@ export function applyNewModelPolicy(options: {
|
|
|
44
55
|
policy: NewModelPolicy;
|
|
45
56
|
hasSelectedModels?: boolean;
|
|
46
57
|
now: string;
|
|
58
|
+
/** See {@link NewModelPolicyMode}; defaults to `converge`. */
|
|
59
|
+
mode?: NewModelPolicyMode;
|
|
47
60
|
}): NewModelPolicyResult {
|
|
48
61
|
const discovered = [...new Set(options.discoveredIds)].sort();
|
|
49
62
|
const prior = options.baseline;
|
|
@@ -60,8 +73,17 @@ export function applyNewModelPolicy(options: {
|
|
|
60
73
|
const seen = new Set(discovered);
|
|
61
74
|
const newIds = discovered.filter(id => !active.has(id) && !removed.has(id));
|
|
62
75
|
const missing: Record<string, number> = {};
|
|
76
|
+
// Read-side discovery must not advance the removal grace: a per-poll increment would retire a
|
|
77
|
+
// transiently absent id after three polls. Absence keeps its recorded count (and a reappearance
|
|
78
|
+
// clears it) instead of moving toward `removed`.
|
|
79
|
+
const discovery = options.mode === "discovery";
|
|
63
80
|
for (const id of active) {
|
|
64
81
|
if (seen.has(id)) continue;
|
|
82
|
+
if (discovery) {
|
|
83
|
+
const priorCount = prior.missing?.[id];
|
|
84
|
+
if (priorCount !== undefined) missing[id] = priorCount;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
65
87
|
const count = (prior.missing?.[id] ?? 0) + 1;
|
|
66
88
|
if (count >= MODEL_REMOVAL_GRACE_FETCHES) {
|
|
67
89
|
active.delete(id);
|
|
@@ -99,6 +121,8 @@ export function reconcileSuccessfulModelDiscoveries(options: {
|
|
|
99
121
|
models: Iterable<{ provider: string; id: string; custom?: boolean }>;
|
|
100
122
|
authoritativeProviders: Iterable<string>;
|
|
101
123
|
now: string;
|
|
124
|
+
/** See {@link NewModelPolicyMode}; defaults to `converge`. */
|
|
125
|
+
mode?: NewModelPolicyMode;
|
|
102
126
|
}): boolean {
|
|
103
127
|
const byProvider = new Map<string, string[]>();
|
|
104
128
|
for (const model of options.models) {
|
|
@@ -118,6 +142,7 @@ export function reconcileSuccessfulModelDiscoveries(options: {
|
|
|
118
142
|
policy: effectiveNewModelPolicy(options.config, provider),
|
|
119
143
|
hasSelectedModels: (configured.selectedModels?.length ?? 0) > 0,
|
|
120
144
|
now: options.now,
|
|
145
|
+
mode: options.mode,
|
|
121
146
|
});
|
|
122
147
|
if (result.overflow) continue;
|
|
123
148
|
const priorBaseline = known[provider];
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ANTHROPIC_MODEL_CONTEXT_WINDOWS,
|
|
21
21
|
ANTHROPIC_MODEL_INPUT_MODALITIES,
|
|
22
22
|
ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
|
|
23
|
+
ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS,
|
|
23
24
|
ANTHROPIC_MODEL_REASONING_EFFORTS,
|
|
24
25
|
ZAI_GLM_52_REASONING_EFFORTS,
|
|
25
26
|
ZAI_GLM_53_REASONING_EFFORTS,
|
|
@@ -489,6 +490,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
489
490
|
// Codex omits max_output_tokens; without a provider budget the Anthropic adapter
|
|
490
491
|
// falls back to 8192, which truncates long answers with stop_reason=max_tokens.
|
|
491
492
|
defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
|
|
493
|
+
modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS },
|
|
492
494
|
defaultModel: "claude-sonnet-5",
|
|
493
495
|
// Claude fast mode on the subscription lane (Claude Code `/fast`): the OAuth route accepts
|
|
494
496
|
// `speed` and gates it on account entitlement (usage credits / org enablement), probed live
|
|
@@ -516,6 +518,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
516
518
|
modelInputModalities: { ...ANTHROPIC_MODEL_INPUT_MODALITIES },
|
|
517
519
|
modelReasoningEfforts: { ...ANTHROPIC_MODEL_REASONING_EFFORTS },
|
|
518
520
|
defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
|
|
521
|
+
modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS },
|
|
519
522
|
defaultModel: "claude-sonnet-5",
|
|
520
523
|
fastWire: ANTHROPIC_FAST_WIRE,
|
|
521
524
|
modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS },
|
|
@@ -19,9 +19,38 @@ export const ANTHROPIC_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object
|
|
|
19
19
|
ANTHROPIC_MODELS.map(id => [id, ["text", "image"]]),
|
|
20
20
|
);
|
|
21
21
|
// Every current Claude family accepts at least 64k output tokens (Haiku 4.5 / Sonnet 4.x
|
|
22
|
-
// through Opus 5 and Fable 5)
|
|
23
|
-
//
|
|
22
|
+
// through Opus 5 and Fable 5), so this stays the floor for a model id not listed in
|
|
23
|
+
// ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS below.
|
|
24
24
|
export const ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 64_000;
|
|
25
|
+
/**
|
|
26
|
+
* Per-model synchronous Messages API output maxima, each read from that model's own overview
|
|
27
|
+
* page on platform.claude.com (`/docs/en/models/<slug>/overview`), which states the number in
|
|
28
|
+
* its header line and again in the Capabilities table. Deliberately NOT the 300K Message
|
|
29
|
+
* Batches extended-output beta, which needs `output-300k-2026-03-24` and does not apply to the
|
|
30
|
+
* synchronous route these providers use.
|
|
31
|
+
*
|
|
32
|
+
* Every seeded Claude model except Haiku 4.5 is 128K. Haiku 4.5 is genuinely 64K and is left
|
|
33
|
+
* out so it keeps inheriting ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS.
|
|
34
|
+
*
|
|
35
|
+
* Without this the 64000 default stood in for all of them, which understates the real ceiling
|
|
36
|
+
* by half on every Opus, Sonnet and Fable model. `resolveOutputCeiling` is the value the
|
|
37
|
+
* adapter budgets against when a caller declares no `max_tokens`, so a 128K-capable model
|
|
38
|
+
* stopped at 64000 with `stop_reason: max_tokens`; the same figure feeds the combo admission
|
|
39
|
+
* reserve in `checkComboTargetInputAdmission`, where understating a target's output makes its
|
|
40
|
+
* headroom arithmetic wrong.
|
|
41
|
+
*/
|
|
42
|
+
export const ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
|
43
|
+
"claude-fable-5-1": 128_000,
|
|
44
|
+
"claude-fable-5": 128_000,
|
|
45
|
+
"claude-sonnet-5-5": 128_000,
|
|
46
|
+
"claude-sonnet-5": 128_000,
|
|
47
|
+
"claude-opus-5-5": 128_000,
|
|
48
|
+
"claude-opus-5": 128_000,
|
|
49
|
+
"claude-opus-4-8": 128_000,
|
|
50
|
+
"claude-opus-4-7": 128_000,
|
|
51
|
+
"claude-opus-4-6": 128_000,
|
|
52
|
+
"claude-sonnet-4-6": 128_000,
|
|
53
|
+
};
|
|
25
54
|
/**
|
|
26
55
|
* The effort rungs opencodex exposes for native Anthropic models. Without this the
|
|
27
56
|
* providers advertised no ladder at all, so every client that keys its effort control off
|
|
@@ -28,8 +28,8 @@ export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "bas
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export const XAI_GROK_COMPATIBILITY = {
|
|
31
|
-
version: "0.
|
|
32
|
-
userAgent: "opencodex-grok/0.
|
|
31
|
+
version: "1.0.46",
|
|
32
|
+
userAgent: "opencodex-grok/1.0.46",
|
|
33
33
|
headers: {
|
|
34
34
|
clientIdentifier: "x-grok-client-identifier",
|
|
35
35
|
clientVersion: "x-grok-client-version",
|
|
@@ -555,6 +555,14 @@ async function anthropicNativePassthrough(
|
|
|
555
555
|
logCtx.provider = "anthropic-native";
|
|
556
556
|
logCtx.requestedModel = model;
|
|
557
557
|
const finalize = createFinalRequestLog(logIds, logCtx).finish;
|
|
558
|
+
// Locally generated diagnostics are fixed text plus validated guard limits. Runtime
|
|
559
|
+
// fetch errors retain their existing client response but use a fixed stored reason.
|
|
560
|
+
const fail = (status: number, closeReason: PassthroughCloseReason | "non_stream", message: string, type: string, storedReason = message) => {
|
|
561
|
+
const safeMessage = redactSecretString(message);
|
|
562
|
+
logCtx.upstreamError = storedReason.slice(0, 500);
|
|
563
|
+
finalize(status, { closeReason });
|
|
564
|
+
return anthropicErrorResponse(status, safeMessage, type);
|
|
565
|
+
};
|
|
558
566
|
|
|
559
567
|
const base = (config.claudeCode?.anthropicBaseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
560
568
|
const search = new URL(req.url).search;
|
|
@@ -581,13 +589,11 @@ async function anthropicNativePassthrough(
|
|
|
581
589
|
req.signal,
|
|
582
590
|
);
|
|
583
591
|
if (result.kind === "timeout") {
|
|
584
|
-
|
|
585
|
-
return anthropicErrorResponse(504, "anthropic passthrough timed out waiting for response headers", "timeout_error");
|
|
592
|
+
return fail(504, "non_stream", "anthropic passthrough timed out waiting for response headers", "timeout_error");
|
|
586
593
|
}
|
|
587
594
|
if (result.kind === "error") {
|
|
588
595
|
const err = result.error;
|
|
589
|
-
|
|
590
|
-
return anthropicErrorResponse(502, `anthropic passthrough failed: ${err instanceof Error ? err.message : String(err)}`, "api_error");
|
|
596
|
+
return fail(502, "non_stream", `anthropic passthrough failed: ${err instanceof Error ? err.message : String(err)}`, "api_error", "anthropic passthrough failed: upstream connection error");
|
|
591
597
|
}
|
|
592
598
|
const upstream = result.upstream;
|
|
593
599
|
|
|
@@ -607,16 +613,13 @@ async function anthropicNativePassthrough(
|
|
|
607
613
|
// idle/size bounds — headers are NOT yet sent here, so real statuses are available.
|
|
608
614
|
const bodyResult = await readBoundedPassthroughBody(upstream, bodyGuard);
|
|
609
615
|
if (bodyResult.kind === "client_cancel") {
|
|
610
|
-
|
|
611
|
-
return anthropicErrorResponse(499, "client closed request during anthropic passthrough", "api_error");
|
|
616
|
+
return fail(499, "client_cancel", "client closed request during anthropic passthrough", "api_error");
|
|
612
617
|
}
|
|
613
618
|
if (bodyResult.kind === "stall") {
|
|
614
|
-
|
|
615
|
-
return anthropicErrorResponse(504, `anthropic passthrough body stalled: no upstream bytes for ${Math.round(bodyGuard.stallMs / 1000)}s`, "timeout_error");
|
|
619
|
+
return fail(504, "body_stall", `anthropic passthrough body stalled: no upstream bytes for ${Math.round(bodyGuard.stallMs / 1000)}s`, "timeout_error");
|
|
616
620
|
}
|
|
617
621
|
if (bodyResult.kind === "overflow") {
|
|
618
|
-
|
|
619
|
-
return anthropicErrorResponse(502, `anthropic passthrough body exceeded ${bodyGuard.maxBytes} bytes`, "api_error");
|
|
622
|
+
return fail(502, "body_overflow", `anthropic passthrough body exceeded ${bodyGuard.maxBytes} bytes`, "api_error");
|
|
620
623
|
}
|
|
621
624
|
const text = bodyResult.text;
|
|
622
625
|
if (upstream.ok) {
|
|
@@ -624,6 +627,21 @@ async function anthropicNativePassthrough(
|
|
|
624
627
|
const parsed = JSON.parse(text) as { usage?: Rec };
|
|
625
628
|
if (isRec(parsed?.usage)) logCtx.usage = anthropicUsageToOcx(parsed.usage);
|
|
626
629
|
} catch { /* count_tokens etc. */ }
|
|
630
|
+
} else if (upstream.status >= 400) {
|
|
631
|
+
// Upstream text is untrusted: retain only a closed type vocabulary in history.
|
|
632
|
+
// Bound parsing work independently of the larger response-relay body limit.
|
|
633
|
+
const statusReason = `Provider error ${upstream.status}`;
|
|
634
|
+
logCtx.upstreamError = statusReason;
|
|
635
|
+
if (text.length <= PASSTHROUGH_ERROR_DETAIL_MAX_CHARS) {
|
|
636
|
+
try {
|
|
637
|
+
const parsed: unknown = JSON.parse(text);
|
|
638
|
+
if (isRec(parsed) && parsed.type === "error" && isRec(parsed.error)) {
|
|
639
|
+
const upstreamType = parsed.error.type;
|
|
640
|
+
const knownType = PASSTHROUGH_ERROR_TYPES.find(type => type === upstreamType);
|
|
641
|
+
if (knownType) logCtx.upstreamError = `${statusReason}: ${knownType}`;
|
|
642
|
+
}
|
|
643
|
+
} catch { /* Malformed errors keep the fixed status-only diagnostic. */ }
|
|
644
|
+
}
|
|
627
645
|
}
|
|
628
646
|
finalize(upstream.status, { closeReason: "non_stream" });
|
|
629
647
|
const retryAfter = upstream.headers.get("retry-after");
|
|
@@ -633,6 +651,11 @@ async function anthropicNativePassthrough(
|
|
|
633
651
|
});
|
|
634
652
|
}
|
|
635
653
|
|
|
654
|
+
const PASSTHROUGH_ERROR_TYPES = [
|
|
655
|
+
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
656
|
+
"rate_limit_error", "api_error", "overloaded_error", "request_too_large",
|
|
657
|
+
] as const;
|
|
658
|
+
const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
|
|
636
659
|
const DEFAULT_BODY_STALL_SEC = 90;
|
|
637
660
|
const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
|
|
638
661
|
|
|
@@ -806,8 +806,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
806
806
|
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
807
807
|
// Evaluate visibility AFTER discovery: a concurrent visibility write may have completed.
|
|
808
808
|
const visible = new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")));
|
|
809
|
+
// A bare native id orders the complete picker (docs: guides/model-ordering.md).
|
|
810
|
+
for (const row of nativeModelRows(config)) if (!row.disabled) visible.add(row.slug);
|
|
809
811
|
if (pickerOrder.some(model => !visible.has(model))) {
|
|
810
|
-
return jsonResponse({ error: "pickerOrder must contain
|
|
812
|
+
return jsonResponse({ error: "pickerOrder must contain visible routed or native model ids, each at most once" }, 400);
|
|
811
813
|
}
|
|
812
814
|
}
|
|
813
815
|
}
|
|
@@ -12,6 +12,7 @@ import type { CatalogModel } from "../../codex/catalog";
|
|
|
12
12
|
import { observeModelCacheRevision } from "../../codex/model-cache";
|
|
13
13
|
import {
|
|
14
14
|
captureExportConfigAdmission,
|
|
15
|
+
detachedConfigSnapshot,
|
|
15
16
|
isExportConfigAdmissionCurrent,
|
|
16
17
|
type ExportConfigAdmission,
|
|
17
18
|
} from "../../config/admitted-identity";
|
|
@@ -37,6 +38,9 @@ import type { OcxConfig } from "../../types";
|
|
|
37
38
|
import { enrichProviderFromRegistry } from "../../providers/derive";
|
|
38
39
|
import { ensureCodexEntitlementFreshness } from "../../codex/model-entitlements";
|
|
39
40
|
import { fetchAllModels } from "./shared";
|
|
41
|
+
import { CatalogGatherBusyError } from "../../codex/catalog/routed-gather";
|
|
42
|
+
import { captureModelDiscoveryBaseline, finalizeModelDiscovery } from "../../providers/new-model-policy-runtime";
|
|
43
|
+
import { reconcileSuccessfulModelDiscoveries } from "../../providers/new-model-policy";
|
|
40
44
|
import { initialModelSelectionPending, pendingModelSelectionProviders } from "../../providers/initial-model-selection";
|
|
41
45
|
import { catalogFastRowEligible, fastRowId } from "../fast-row";
|
|
42
46
|
import { knownEffortRowIds } from "../effort-row";
|
|
@@ -275,7 +279,7 @@ export async function listManagementModelRows(
|
|
|
275
279
|
*/
|
|
276
280
|
const models = options.models === undefined
|
|
277
281
|
? (await Promise.all([
|
|
278
|
-
fetchAllModels(config, options.providerContentRevisions),
|
|
282
|
+
fetchAllModels(config, options.providerContentRevisions, projection => { config = projection; }),
|
|
279
283
|
ensureCodexEntitlementFreshness(config, {
|
|
280
284
|
waitMs: options.entitlementWaitMs ?? 3_000,
|
|
281
285
|
}),
|
|
@@ -471,8 +475,10 @@ export async function loadExportModels(
|
|
|
471
475
|
// The configuration this pass will use from beginning to end, proved to be the one on disk.
|
|
472
476
|
// Without it there is nothing that may be retained, and the caller still gets its rows: only the
|
|
473
477
|
// preview authority is withheld.
|
|
474
|
-
|
|
475
|
-
|
|
478
|
+
let admission = captureExportConfigAdmission(config);
|
|
479
|
+
let admitted = admission?.config ?? config;
|
|
480
|
+
const discoveryBaseline = models === undefined ? captureModelDiscoveryBaseline(admitted) : null;
|
|
481
|
+
const outcomes: Array<{ provider: string; state: "authoritative" | "degraded" }> = [];
|
|
476
482
|
// The gather stamps each provider as it chooses its rows, so the roster and the revisions that
|
|
477
483
|
// vouch for it come from the same moment. Sampling afterwards would let a concurrent flight's
|
|
478
484
|
// publication be recorded against rows it never produced.
|
|
@@ -483,10 +489,34 @@ export async function loadExportModels(
|
|
|
483
489
|
// had, and runs alongside as it did inside the projection.
|
|
484
490
|
const roster = models === undefined
|
|
485
491
|
? (await Promise.all([
|
|
486
|
-
(await import("../../codex/catalog")).gatherRoutedModels(admitted, {
|
|
492
|
+
(await import("../../codex/catalog")).gatherRoutedModels(admitted, {
|
|
493
|
+
providerContentRevisions: gathered, providerModelOutcomes: outcomes,
|
|
494
|
+
}),
|
|
487
495
|
ensureCodexEntitlementFreshness(admitted, { waitMs: 3_000 }),
|
|
488
496
|
]))[0]
|
|
489
497
|
: models;
|
|
498
|
+
if (discoveryBaseline !== null) {
|
|
499
|
+
const authoritative = outcomes.filter(outcome => outcome.state === "authoritative").map(outcome => outcome.provider);
|
|
500
|
+
if (admission !== null && isExportConfigAdmissionCurrent(admission, config)
|
|
501
|
+
&& [...gathered].every(([provider, revision]) => observeModelCacheRevision(provider) === revision)) {
|
|
502
|
+
if (!finalizeModelDiscovery(config, discoveryBaseline, roster, authoritative, gathered)) {
|
|
503
|
+
lastExportSnapshot = null;
|
|
504
|
+
throw new CatalogGatherBusyError();
|
|
505
|
+
}
|
|
506
|
+
// Only our scoped discovery decision moved: bind the projection to the committed state.
|
|
507
|
+
admission = captureExportConfigAdmission(config);
|
|
508
|
+
admitted = admission?.config ?? config;
|
|
509
|
+
} else {
|
|
510
|
+
// Preserve the old-response contract after a race, without persisting stale evidence or
|
|
511
|
+
// retaining it as a current preview. Apply visibility policy only to a detached projection.
|
|
512
|
+
const projection = detachedConfigSnapshot(admitted);
|
|
513
|
+
if (projection === null) throw new CatalogGatherBusyError();
|
|
514
|
+
reconcileSuccessfulModelDiscoveries({ config: projection, models: roster,
|
|
515
|
+
authoritativeProviders: authoritative, now: new Date().toISOString(), mode: "discovery" });
|
|
516
|
+
admitted = projection;
|
|
517
|
+
admission = null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
490
520
|
const rows = await listManagementModelRows(admitted, { models: roster });
|
|
491
521
|
// Management deliberately lists the full roster so hidden models can be enabled.
|
|
492
522
|
// A client picker must also honor the provider selection, not just its blocklist.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { captureInitialSelectionBaseline, finalizeInitialModelSelection } from "../../providers/initial-model-selection-runtime";
|
|
3
|
+
import { captureModelDiscoveryBaseline, finalizeModelDiscovery } from "../../providers/new-model-policy-runtime";
|
|
4
|
+
import { CatalogGatherBusyError } from "../../codex/catalog/routed-gather";
|
|
3
5
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
4
6
|
import { readFileSync } from "node:fs";
|
|
5
7
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -266,17 +268,31 @@ export async function fetchAllModels(
|
|
|
266
268
|
config: OcxConfig,
|
|
267
269
|
/** Filled with each provider's content revision as of the moment its rows were chosen. */
|
|
268
270
|
providerContentRevisions?: Map<string, string>,
|
|
271
|
+
/** Management renders disabled rows using this detached state after inventory drift. */
|
|
272
|
+
publishProjection?: (projection: OcxConfig) => void,
|
|
269
273
|
): Promise<CatalogModel[]> {
|
|
270
274
|
const { gatherRoutedModels } = await import("../../codex/catalog");
|
|
271
275
|
const baseline = captureInitialSelectionBaseline(config);
|
|
272
|
-
|
|
276
|
+
const discoveryBaseline = captureModelDiscoveryBaseline(config);
|
|
277
|
+
const revisions = providerContentRevisions ?? new Map<string, string>();
|
|
273
278
|
const outcomes: Array<{ provider: string; state: "authoritative" | "degraded" }> = [];
|
|
274
279
|
const models = await gatherRoutedModels(config, {
|
|
275
280
|
providerModelOutcomes: outcomes,
|
|
276
|
-
|
|
281
|
+
providerContentRevisions: revisions,
|
|
277
282
|
});
|
|
278
283
|
finalizeInitialModelSelection(config, baseline, uniqueCatalogModelsForPublicList(models),
|
|
279
284
|
outcomes.filter(outcome => outcome.state === "authoritative").map(outcome => outcome.provider));
|
|
285
|
+
let publicationConfig = config;
|
|
286
|
+
if (!finalizeModelDiscovery(config, discoveryBaseline, models,
|
|
287
|
+
outcomes.filter(outcome => outcome.state === "authoritative").map(outcome => outcome.provider), revisions,
|
|
288
|
+
projection => { publicationConfig = projection; publishProjection?.(projection); })) {
|
|
289
|
+
throw new CatalogGatherBusyError();
|
|
290
|
+
}
|
|
291
|
+
if (publicationConfig !== config && !publishProjection) {
|
|
292
|
+
// Other consumers keep their live config, so only return rows visible under the projection.
|
|
293
|
+
const { filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
294
|
+
return filterCatalogVisibleModels(models, publicationConfig);
|
|
295
|
+
}
|
|
280
296
|
return models;
|
|
281
297
|
}
|
|
282
298
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
|
|
1
2
|
import type { ResponsesRequestContext } from "./core-options";
|
|
2
3
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
4
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -34,8 +35,6 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
|
34
35
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
35
36
|
import {
|
|
36
37
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
37
|
-
rotateAnthropicAccountOn429,
|
|
38
|
-
recordAnthropicAccount429,
|
|
39
38
|
getAnthropicPoolAccessSnapshot,
|
|
40
39
|
formatAnthropicProviderForLog,
|
|
41
40
|
} from "../../oauth/anthropic-routing";
|
|
@@ -158,6 +157,8 @@ export function createAdapterContinuations(
|
|
|
158
157
|
const fetchTerminalGuardContinuation = async function* (
|
|
159
158
|
nextParsed: OcxParsedRequest,
|
|
160
159
|
initialRecoveryKind?: AttemptRecoveryKind,
|
|
160
|
+
// Ordinary terminal continuations have already delivered output; empty retries opt in.
|
|
161
|
+
allowAccountRefusal = false,
|
|
161
162
|
): AsyncGenerator<AdapterEvent> {
|
|
162
163
|
let response: Response | undefined;
|
|
163
164
|
let kiroRefusalPendingReplay: Response | undefined;
|
|
@@ -389,25 +390,21 @@ export function createAdapterContinuations(
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
if (
|
|
392
|
-
response.status === 429
|
|
393
|
+
(response.status === 429 || response.status === 403)
|
|
393
394
|
&& transportState.anthropicPoolAccountId
|
|
394
395
|
&& !isNonReplayableResponse(response)
|
|
395
|
-
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
396
396
|
) {
|
|
397
|
-
const nextAccountId =
|
|
398
|
-
config,
|
|
399
|
-
transportState.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
response.headers,
|
|
404
|
-
transportState.anthropicRouteDecision,
|
|
405
|
-
);
|
|
397
|
+
const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
|
|
398
|
+
config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
|
|
399
|
+
decision: transportState.anthropicRouteDecision, signal: upstream.signal,
|
|
400
|
+
canRetry: transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
401
|
+
allowAccountRefusal,
|
|
402
|
+
});
|
|
406
403
|
if (nextAccountId) {
|
|
407
|
-
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
408
404
|
try {
|
|
409
405
|
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
410
406
|
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
407
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
411
408
|
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
412
409
|
transportState.anthropicPoolFailovers += 1;
|
|
413
410
|
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
@@ -426,11 +423,6 @@ export function createAdapterContinuations(
|
|
|
426
423
|
}
|
|
427
424
|
}
|
|
428
425
|
}
|
|
429
|
-
if (response.status === 429 && transportState.anthropicPoolAccountId
|
|
430
|
-
&& transportState.anthropicPoolFailovers >= ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST) {
|
|
431
|
-
recordAnthropicAccount429(config, transportState.anthropicPoolAccountId,
|
|
432
|
-
response.headers.get("retry-after"), Date.now(), response.headers);
|
|
433
|
-
}
|
|
434
426
|
// Generic OAuth rotation for the continuation loop. The streaming loop grew this arm with
|
|
435
427
|
// #2568 and this one did not, so an xAI/Cursor/Kimi/Copilot/Antigravity/Nous continuation
|
|
436
428
|
// 429 stayed terminal even with failover fully active -- the same class of divergence the
|
|
@@ -704,14 +696,14 @@ export function createAdapterContinuations(
|
|
|
704
696
|
};
|
|
705
697
|
|
|
706
698
|
const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
|
|
707
|
-
const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion");
|
|
699
|
+
const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion", true);
|
|
708
700
|
return terminalGuardEnabled
|
|
709
701
|
? guardTerminalEventStream({
|
|
710
702
|
parsed,
|
|
711
703
|
firstEvents: retryEvents,
|
|
712
704
|
adapterName: transportState.activeAdapter.name,
|
|
713
705
|
maxAutoContinuations: 1,
|
|
714
|
-
continuation: fetchTerminalGuardContinuation,
|
|
706
|
+
continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
|
|
715
707
|
})
|
|
716
708
|
: retryEvents;
|
|
717
709
|
};
|
|
@@ -110,7 +110,7 @@ export async function deliverAdapterResponse(
|
|
|
110
110
|
firstEvents: initialEventStream,
|
|
111
111
|
adapterName: transportState.activeAdapter.name,
|
|
112
112
|
maxAutoContinuations: 1,
|
|
113
|
-
continuation: fetchTerminalGuardContinuation,
|
|
113
|
+
continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
|
|
114
114
|
})
|
|
115
115
|
: initialEventStream;
|
|
116
116
|
// The empty-completion guard sits OUTSIDE the terminal guard: a completed
|
|
@@ -235,7 +235,7 @@ export async function deliverAdapterResponse(
|
|
|
235
235
|
firstEvents: (async function* () { yield* initialEvents; })(),
|
|
236
236
|
adapterName: transportState.activeAdapter.name,
|
|
237
237
|
maxAutoContinuations: 1,
|
|
238
|
-
continuation: fetchTerminalGuardContinuation,
|
|
238
|
+
continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
|
|
239
239
|
})) guardedEvents.push(event);
|
|
240
240
|
} else {
|
|
241
241
|
guardedEvents = initialEvents;
|