@sayknow-cli/ai 0.3.16 → 0.4.1
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/package.json +24 -25
- package/src/auth-gateway/server.ts +164 -22
- package/src/auth-storage.ts +76 -43
- package/src/context-cap-policy.ts +59 -0
- package/src/index.ts +2 -0
- package/src/model-cache.ts +10 -0
- package/src/model-manager.ts +30 -17
- package/src/model-thinking.ts +14 -8
- package/src/providers/amazon-bedrock.ts +9 -1
- package/src/providers/anthropic-messages-server.ts +178 -17
- package/src/providers/anthropic.ts +199 -213
- package/src/providers/azure-openai-responses.ts +6 -1
- package/src/providers/google-gemini-cli.ts +26 -13
- package/src/providers/google-shared.ts +7 -1
- package/src/providers/ollama.ts +7 -1
- package/src/providers/openai-chat-server.ts +93 -5
- package/src/providers/openai-codex/response-handler.ts +11 -3
- package/src/providers/openai-codex-responses.ts +107 -20
- package/src/providers/openai-responses-server.ts +111 -36
- package/src/providers/openai-responses-shared.ts +71 -12
- package/src/providers/openai-responses.ts +26 -3
- package/src/providers/pi-native-client.ts +24 -12
- package/src/providers/pi-native-server.ts +275 -21
- package/src/types.ts +53 -4
- package/src/utils/discovery/codex.ts +3 -12
- package/src/utils/event-stream.ts +146 -58
- package/src/utils/fallback-transport.ts +269 -0
- package/src/utils/overflow.ts +72 -31
- package/src/utils/retry.ts +2 -2
- package/src/utils/validation.ts +6 -1
- package/src/utils.ts +120 -12
- package/dist/types/api-registry.d.ts +0 -30
- package/dist/types/auth-broker/client.d.ts +0 -67
- package/dist/types/auth-broker/index.d.ts +0 -5
- package/dist/types/auth-broker/refresher.d.ts +0 -25
- package/dist/types/auth-broker/remote-store.d.ts +0 -99
- package/dist/types/auth-broker/server.d.ts +0 -32
- package/dist/types/auth-broker/types.d.ts +0 -110
- package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
- package/dist/types/auth-gateway/http.d.ts +0 -40
- package/dist/types/auth-gateway/index.d.ts +0 -3
- package/dist/types/auth-gateway/server.d.ts +0 -17
- package/dist/types/auth-gateway/types.d.ts +0 -115
- package/dist/types/auth-storage.d.ts +0 -695
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/index.d.ts +0 -51
- package/dist/types/model-cache.d.ts +0 -17
- package/dist/types/model-manager.d.ts +0 -62
- package/dist/types/model-retirements.d.ts +0 -6
- package/dist/types/model-thinking.d.ts +0 -74
- package/dist/types/models.d.ts +0 -21
- package/dist/types/provider-details.d.ts +0 -24
- package/dist/types/provider-models/bundled-references.d.ts +0 -4
- package/dist/types/provider-models/descriptors.d.ts +0 -48
- package/dist/types/provider-models/google.d.ts +0 -20
- package/dist/types/provider-models/index.d.ts +0 -5
- package/dist/types/provider-models/ollama.d.ts +0 -7
- package/dist/types/provider-models/openai-compat.d.ts +0 -256
- package/dist/types/provider-models/special.d.ts +0 -19
- package/dist/types/providers/amazon-bedrock.d.ts +0 -60
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
- package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
- package/dist/types/providers/anthropic.d.ts +0 -208
- package/dist/types/providers/aws-credential-config.d.ts +0 -19
- package/dist/types/providers/aws-credentials.d.ts +0 -43
- package/dist/types/providers/aws-eventstream.d.ts +0 -38
- package/dist/types/providers/aws-sigv4.d.ts +0 -55
- package/dist/types/providers/azure-openai-responses.d.ts +0 -15
- package/dist/types/providers/composer-discipline.d.ts +0 -26
- package/dist/types/providers/cursor/client-version.d.ts +0 -10
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
- package/dist/types/providers/cursor.d.ts +0 -45
- package/dist/types/providers/error-message.d.ts +0 -27
- package/dist/types/providers/github-copilot-headers.d.ts +0 -40
- package/dist/types/providers/gitlab-duo.d.ts +0 -27
- package/dist/types/providers/google-auth.d.ts +0 -24
- package/dist/types/providers/google-gemini-cli.d.ts +0 -75
- package/dist/types/providers/google-gemini-headers.d.ts +0 -43
- package/dist/types/providers/google-shared.d.ts +0 -179
- package/dist/types/providers/google-types.d.ts +0 -138
- package/dist/types/providers/google-vertex.d.ts +0 -7
- package/dist/types/providers/google.d.ts +0 -4
- package/dist/types/providers/grammar.d.ts +0 -1
- package/dist/types/providers/kimi.d.ts +0 -27
- package/dist/types/providers/mock.d.ts +0 -177
- package/dist/types/providers/ollama.d.ts +0 -41
- package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
- package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
- package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
- package/dist/types/providers/openai-chat-server.d.ts +0 -16
- package/dist/types/providers/openai-codex/constants.d.ts +0 -26
- package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
- package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
- package/dist/types/providers/openai-codex-responses.d.ts +0 -67
- package/dist/types/providers/openai-completions-compat.d.ts +0 -27
- package/dist/types/providers/openai-completions.d.ts +0 -33
- package/dist/types/providers/openai-request-transform.d.ts +0 -4
- package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
- package/dist/types/providers/openai-responses-server.d.ts +0 -17
- package/dist/types/providers/openai-responses-shared.d.ts +0 -104
- package/dist/types/providers/openai-responses.d.ts +0 -32
- package/dist/types/providers/pi-native-client.d.ts +0 -13
- package/dist/types/providers/pi-native-server.d.ts +0 -68
- package/dist/types/providers/register-builtins.d.ts +0 -31
- package/dist/types/providers/synthetic.d.ts +0 -26
- package/dist/types/providers/transform-messages.d.ts +0 -14
- package/dist/types/providers/vision-guard.d.ts +0 -8
- package/dist/types/rate-limit-utils.d.ts +0 -19
- package/dist/types/stream.d.ts +0 -43
- package/dist/types/types.d.ts +0 -831
- package/dist/types/usage/claude.d.ts +0 -3
- package/dist/types/usage/gemini.d.ts +0 -2
- package/dist/types/usage/github-copilot.d.ts +0 -7
- package/dist/types/usage/google-antigravity.d.ts +0 -2
- package/dist/types/usage/grok-cli.d.ts +0 -10
- package/dist/types/usage/kimi.d.ts +0 -2
- package/dist/types/usage/minimax-code.d.ts +0 -2
- package/dist/types/usage/openai-codex.d.ts +0 -3
- package/dist/types/usage/shared.d.ts +0 -1
- package/dist/types/usage/zai.d.ts +0 -2
- package/dist/types/usage.d.ts +0 -258
- package/dist/types/utils/abort.d.ts +0 -19
- package/dist/types/utils/anthropic-auth.d.ts +0 -31
- package/dist/types/utils/discovery/antigravity.d.ts +0 -67
- package/dist/types/utils/discovery/codex.d.ts +0 -38
- package/dist/types/utils/discovery/cursor.d.ts +0 -23
- package/dist/types/utils/discovery/gemini.d.ts +0 -25
- package/dist/types/utils/discovery/index.d.ts +0 -4
- package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
- package/dist/types/utils/event-stream.d.ts +0 -33
- package/dist/types/utils/fireworks-model-id.d.ts +0 -10
- package/dist/types/utils/foundry.d.ts +0 -1
- package/dist/types/utils/h2-fetch.d.ts +0 -22
- package/dist/types/utils/http-inspector.d.ts +0 -35
- package/dist/types/utils/idle-iterator.d.ts +0 -67
- package/dist/types/utils/json-parse.d.ts +0 -18
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
- package/dist/types/utils/oauth/anthropic.d.ts +0 -22
- package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
- package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
- package/dist/types/utils/oauth/callback-server.d.ts +0 -60
- package/dist/types/utils/oauth/cerebras.d.ts +0 -1
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/cursor.d.ts +0 -15
- package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
- package/dist/types/utils/oauth/deepseek.d.ts +0 -10
- package/dist/types/utils/oauth/firepass.d.ts +0 -1
- package/dist/types/utils/oauth/fireworks.d.ts +0 -1
- package/dist/types/utils/oauth/fugu.d.ts +0 -1
- package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
- package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
- package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
- package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
- package/dist/types/utils/oauth/huggingface.d.ts +0 -19
- package/dist/types/utils/oauth/index.d.ts +0 -39
- package/dist/types/utils/oauth/kagi.d.ts +0 -17
- package/dist/types/utils/oauth/kilo.d.ts +0 -5
- package/dist/types/utils/oauth/kimi.d.ts +0 -21
- package/dist/types/utils/oauth/litellm.d.ts +0 -18
- package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
- package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
- package/dist/types/utils/oauth/moonshot.d.ts +0 -1
- package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
- package/dist/types/utils/oauth/nvidia.d.ts +0 -18
- package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
- package/dist/types/utils/oauth/ollama.d.ts +0 -18
- package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
- package/dist/types/utils/oauth/opencode.d.ts +0 -18
- package/dist/types/utils/oauth/parallel.d.ts +0 -17
- package/dist/types/utils/oauth/perplexity.d.ts +0 -9
- package/dist/types/utils/oauth/pkce.d.ts +0 -8
- package/dist/types/utils/oauth/qianfan.d.ts +0 -17
- package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
- package/dist/types/utils/oauth/synthetic.d.ts +0 -1
- package/dist/types/utils/oauth/tavily.d.ts +0 -17
- package/dist/types/utils/oauth/together.d.ts +0 -1
- package/dist/types/utils/oauth/types.d.ts +0 -45
- package/dist/types/utils/oauth/venice.d.ts +0 -18
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/vllm.d.ts +0 -16
- package/dist/types/utils/oauth/xai.d.ts +0 -30
- package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
- package/dist/types/utils/oauth/zai.d.ts +0 -18
- package/dist/types/utils/oauth/zenmux.d.ts +0 -1
- package/dist/types/utils/overflow.d.ts +0 -64
- package/dist/types/utils/parse-bind.d.ts +0 -23
- package/dist/types/utils/provider-response.d.ts +0 -3
- package/dist/types/utils/retry-after.d.ts +0 -3
- package/dist/types/utils/retry-budget.d.ts +0 -1
- package/dist/types/utils/retry.d.ts +0 -26
- package/dist/types/utils/schema/adapt.d.ts +0 -24
- package/dist/types/utils/schema/compatibility.d.ts +0 -30
- package/dist/types/utils/schema/dereference.d.ts +0 -11
- package/dist/types/utils/schema/draft.d.ts +0 -10
- package/dist/types/utils/schema/equality.d.ts +0 -4
- package/dist/types/utils/schema/fields.d.ts +0 -49
- package/dist/types/utils/schema/index.d.ts +0 -14
- package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
- package/dist/types/utils/schema/meta-validator.d.ts +0 -2
- package/dist/types/utils/schema/normalize.d.ts +0 -93
- package/dist/types/utils/schema/root-combinator.d.ts +0 -12
- package/dist/types/utils/schema/spill.d.ts +0 -8
- package/dist/types/utils/schema/stamps.d.ts +0 -25
- package/dist/types/utils/schema/types.d.ts +0 -4
- package/dist/types/utils/schema/wire.d.ts +0 -54
- package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
- package/dist/types/utils/sse-debug.d.ts +0 -10
- package/dist/types/utils/tool-call-healing.d.ts +0 -71
- package/dist/types/utils/tool-choice-capability.d.ts +0 -41
- package/dist/types/utils/tool-choice.d.ts +0 -50
- package/dist/types/utils/validation.d.ts +0 -17
- package/dist/types/utils.d.ts +0 -35
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import type { AssistantMessage, AssistantMessageEvent } from "../types";
|
|
2
2
|
|
|
3
|
+
interface EventQueueNode<T> {
|
|
4
|
+
type: "event";
|
|
5
|
+
event: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type QueueNode<T> = EventQueueNode<T> | { type: "consumer-drain"; drain: ConsumerDrain };
|
|
9
|
+
|
|
10
|
+
interface ConsumerDrain {
|
|
11
|
+
settled: boolean;
|
|
12
|
+
signal: AbortSignal;
|
|
13
|
+
abortListener: () => void;
|
|
14
|
+
resolve: () => void;
|
|
15
|
+
reject: (reason: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
19
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
// Generic event stream class for async iteration
|
|
23
|
+
|
|
4
24
|
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
5
|
-
#queue: T[] = [];
|
|
25
|
+
#queue: QueueNode<T>[] = [];
|
|
6
26
|
#queueHead = 0;
|
|
7
27
|
waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
|
|
28
|
+
#pendingConsumerDrains = new Set<ConsumerDrain>();
|
|
29
|
+
#activeConsumerCount = 0;
|
|
8
30
|
done = false;
|
|
9
31
|
#failed = false;
|
|
10
32
|
#error: unknown = undefined;
|
|
@@ -26,20 +48,20 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
26
48
|
this.extractResult = extractResult;
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
#enqueue(
|
|
30
|
-
this.#queue.push(
|
|
51
|
+
#enqueue(node: QueueNode<T>): void {
|
|
52
|
+
this.#queue.push(node);
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
#dequeue(): T | undefined {
|
|
55
|
+
#dequeue(): QueueNode<T> | undefined {
|
|
34
56
|
if (this.#queueHead >= this.#queue.length) return undefined;
|
|
35
|
-
const
|
|
36
|
-
this.#queue[this.#queueHead] = undefined as T
|
|
57
|
+
const node = this.#queue[this.#queueHead]!;
|
|
58
|
+
this.#queue[this.#queueHead] = undefined as unknown as QueueNode<T>;
|
|
37
59
|
this.#queueHead++;
|
|
38
60
|
if (this.#queueHead > 1024 && this.#queueHead * 2 >= this.#queue.length) {
|
|
39
61
|
this.#queue = this.#queue.slice(this.#queueHead);
|
|
40
62
|
this.#queueHead = 0;
|
|
41
63
|
}
|
|
42
|
-
return
|
|
64
|
+
return node;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
get #queueLength(): number {
|
|
@@ -49,40 +71,114 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
49
71
|
/**
|
|
50
72
|
* Read-only snapshot of the not-yet-consumed events. Always a fresh copy:
|
|
51
73
|
* external code can never mutate internal queue state or observe head-index
|
|
52
|
-
* tombstones, so the deque cannot desynchronize.
|
|
74
|
+
* tombstones or private consumer-drain sentinels, so the deque cannot desynchronize.
|
|
53
75
|
*/
|
|
54
76
|
get queue(): T[] {
|
|
55
|
-
return this.#queue.slice(this.#queueHead);
|
|
77
|
+
return this.#queue.slice(this.#queueHead).flatMap(node => (node.type === "event" ? [node.event] : []));
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
/** Read-only test seam for outstanding consumer-drain waiters. */
|
|
81
|
+
get pendingConsumerDrainCountForTests(): number {
|
|
82
|
+
return this.#pendingConsumerDrains.size;
|
|
83
|
+
}
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
#settleConsumerDrain(drain: ConsumerDrain, status: "resolve" | "reject", reason?: unknown): void {
|
|
86
|
+
if (drain.settled) return;
|
|
87
|
+
drain.settled = true;
|
|
88
|
+
this.#pendingConsumerDrains.delete(drain);
|
|
89
|
+
drain.signal.removeEventListener("abort", drain.abortListener);
|
|
90
|
+
if (status === "resolve") {
|
|
91
|
+
drain.resolve();
|
|
92
|
+
} else {
|
|
93
|
+
drain.reject(reason);
|
|
64
94
|
}
|
|
95
|
+
}
|
|
65
96
|
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
waiter.resolve({ value: event, done: false });
|
|
70
|
-
} else {
|
|
71
|
-
this.#enqueue(event);
|
|
97
|
+
#settleAllConsumerDrains(status: "resolve" | "reject", reason?: unknown): void {
|
|
98
|
+
for (const drain of this.#pendingConsumerDrains) {
|
|
99
|
+
this.#settleConsumerDrain(drain, status, reason);
|
|
72
100
|
}
|
|
73
101
|
}
|
|
74
102
|
|
|
103
|
+
#drainQueuedNodesToWaitingConsumers(): void {
|
|
104
|
+
while (this.waiting.length > 0 && this.#queueLength > 0) {
|
|
105
|
+
const node = this.#dequeue()!;
|
|
106
|
+
if (node.type === "consumer-drain") {
|
|
107
|
+
this.#settleConsumerDrain(node.drain, "resolve");
|
|
108
|
+
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
this.waiting.shift()!.resolve({ value: node.event, done: false });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#dequeueEvent(): EventQueueNode<T> | undefined {
|
|
116
|
+
while (this.#queueLength > 0) {
|
|
117
|
+
const node = this.#dequeue()!;
|
|
118
|
+
if (node.type === "event") return node;
|
|
119
|
+
this.#settleConsumerDrain(node.drain, "resolve");
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
push(event: T): void {
|
|
125
|
+
if (this.done) return;
|
|
126
|
+
try {
|
|
127
|
+
if (this.isComplete(event)) {
|
|
128
|
+
const result = this.extractResult(event);
|
|
129
|
+
this.done = true;
|
|
130
|
+
this.resolveFinalResult(result);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.fail(error);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.deliver(event);
|
|
137
|
+
}
|
|
138
|
+
|
|
75
139
|
deliver(event: T): void {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
waiter
|
|
79
|
-
|
|
80
|
-
|
|
140
|
+
if (this.#queueLength === 0) {
|
|
141
|
+
const waiter = this.waiting.shift();
|
|
142
|
+
if (waiter) {
|
|
143
|
+
waiter.resolve({ value: event, done: false });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
81
146
|
}
|
|
147
|
+
this.#enqueue({ type: "event", event });
|
|
148
|
+
this.#drainQueuedNodesToWaitingConsumers();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolves after every event enqueued before this call has been yielded and
|
|
153
|
+
* the consumer asks the iterator for its next node. The private sentinel is
|
|
154
|
+
* never exposed through the async iterator.
|
|
155
|
+
*/
|
|
156
|
+
waitForConsumerDrain(signal: AbortSignal): Promise<void> {
|
|
157
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
158
|
+
if (this.#failed) return Promise.reject(this.#error);
|
|
159
|
+
if (this.done && this.#activeConsumerCount === 0) {
|
|
160
|
+
if (this.#queueLength === 0) return Promise.resolve();
|
|
161
|
+
return Promise.reject(new Error("Event stream ended before queued events could be drained"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
165
|
+
let drain!: ConsumerDrain;
|
|
166
|
+
const abortListener = () => this.#settleConsumerDrain(drain, "reject", abortReason(signal));
|
|
167
|
+
|
|
168
|
+
drain = { settled: false, signal, abortListener, resolve, reject };
|
|
169
|
+
this.#pendingConsumerDrains.add(drain);
|
|
170
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
171
|
+
this.#enqueue({ type: "consumer-drain", drain });
|
|
172
|
+
this.#drainQueuedNodesToWaitingConsumers();
|
|
173
|
+
return promise;
|
|
82
174
|
}
|
|
83
175
|
|
|
84
176
|
end(result?: R): void {
|
|
85
177
|
this.done = true;
|
|
178
|
+
if (this.#activeConsumerCount === 0) {
|
|
179
|
+
this.#settleAllConsumerDrains("reject", new Error("Event stream ended before consumer drain completed"));
|
|
180
|
+
}
|
|
181
|
+
|
|
86
182
|
if (result !== undefined) {
|
|
87
183
|
this.resolveFinalResult(result);
|
|
88
184
|
}
|
|
@@ -94,6 +190,9 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
94
190
|
}
|
|
95
191
|
|
|
96
192
|
endWaiting(): void {
|
|
193
|
+
if (this.#activeConsumerCount === 0) {
|
|
194
|
+
this.#settleAllConsumerDrains("reject", new Error("Event stream ended before consumer drain completed"));
|
|
195
|
+
}
|
|
97
196
|
while (this.waiting.length > 0) {
|
|
98
197
|
const waiter = this.waiting.shift()!;
|
|
99
198
|
waiter.resolve({ value: undefined as any, done: true });
|
|
@@ -105,6 +204,8 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
105
204
|
this.done = true;
|
|
106
205
|
this.#failed = true;
|
|
107
206
|
this.#error = err;
|
|
207
|
+
this.#settleAllConsumerDrains("reject", err);
|
|
208
|
+
|
|
108
209
|
this.rejectFinalResult(err);
|
|
109
210
|
while (this.waiting.length > 0) {
|
|
110
211
|
const waiter = this.waiting.shift()!;
|
|
@@ -113,20 +214,27 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
113
214
|
}
|
|
114
215
|
|
|
115
216
|
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
217
|
+
this.#activeConsumerCount += 1;
|
|
218
|
+
try {
|
|
219
|
+
while (true) {
|
|
220
|
+
const node = this.#dequeueEvent();
|
|
221
|
+
if (node !== undefined) {
|
|
222
|
+
yield node.event;
|
|
223
|
+
} else if (this.#failed) {
|
|
224
|
+
throw this.#error;
|
|
225
|
+
} else if (this.done) {
|
|
226
|
+
return;
|
|
227
|
+
} else {
|
|
228
|
+
const result = await new Promise<IteratorResult<T>>((resolve, reject) =>
|
|
229
|
+
this.waiting.push({ resolve, reject }),
|
|
230
|
+
);
|
|
231
|
+
if (result.done) return;
|
|
232
|
+
yield result.value;
|
|
233
|
+
}
|
|
129
234
|
}
|
|
235
|
+
} finally {
|
|
236
|
+
this.#activeConsumerCount -= 1;
|
|
237
|
+
this.#settleAllConsumerDrains("reject", new Error("Event stream consumer stopped before drain completed"));
|
|
130
238
|
}
|
|
131
239
|
}
|
|
132
240
|
|
|
@@ -149,24 +257,4 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
149
257
|
},
|
|
150
258
|
);
|
|
151
259
|
}
|
|
152
|
-
|
|
153
|
-
override push(event: AssistantMessageEvent): void {
|
|
154
|
-
if (this.done) return;
|
|
155
|
-
|
|
156
|
-
// Completion resolves the final result and still emits the terminal event.
|
|
157
|
-
if (this.isComplete(event)) {
|
|
158
|
-
this.done = true;
|
|
159
|
-
this.resolveFinalResult(this.extractResult(event));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
this.deliver(event);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
override end(result?: AssistantMessage): void {
|
|
166
|
-
this.done = true;
|
|
167
|
-
if (result !== undefined) {
|
|
168
|
-
this.resolveFinalResult(result);
|
|
169
|
-
}
|
|
170
|
-
this.endWaiting();
|
|
171
|
-
}
|
|
172
260
|
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
export type FallbackTriggerClass = "rate_limit" | "quota" | "auth" | "server" | "unknown" | "other";
|
|
2
|
+
|
|
3
|
+
export interface FallbackTrigger {
|
|
4
|
+
class: FallbackTriggerClass;
|
|
5
|
+
retryAfterMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TransportHeaders = Headers | Record<string, string | undefined>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Structured facts from an upstream HTTP or transport failure. Retry decisions
|
|
12
|
+
* must use these facts rather than provider- or application-owned error text.
|
|
13
|
+
*
|
|
14
|
+
* `headers` is always a plain record limited to the retained retry-signal
|
|
15
|
+
* entries: facts travel on persisted `AssistantMessage`s and through
|
|
16
|
+
* `structuredClone` snapshots (managed fallback attempt staging), so they must
|
|
17
|
+
* never carry a live `Headers` instance — cloning one throws `DataCloneError`
|
|
18
|
+
* ("The object can not be cloned.") and masks the real provider failure.
|
|
19
|
+
*/
|
|
20
|
+
export interface TransportFailureFacts {
|
|
21
|
+
kind: "transport";
|
|
22
|
+
status?: number;
|
|
23
|
+
/** Canonical provider error code used for fallback classification. */
|
|
24
|
+
providerCode?: string;
|
|
25
|
+
/** Anthropic's typed `error.type`, preserved separately at the transport boundary. */
|
|
26
|
+
anthropicErrorType?: string;
|
|
27
|
+
/** OpenAI's typed `error.code`, preserved separately at the transport boundary. */
|
|
28
|
+
openaiErrorCode?: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Opaque per-invocation marker required by managed fallback transport calls. */
|
|
33
|
+
export interface FallbackAttemptToken {
|
|
34
|
+
readonly modelKey: string;
|
|
35
|
+
readonly attemptId: string | number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const issuedAttemptTokens = new WeakSet<object>();
|
|
39
|
+
const consumedAttemptTokens = new WeakSet<object>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Marks a single outer fallback invocation. Accounting belongs to the caller;
|
|
43
|
+
* this token prevents managed transport calls from silently bypassing it.
|
|
44
|
+
*/
|
|
45
|
+
export function beginAttempt(modelKey: string, attemptId: string | number): FallbackAttemptToken {
|
|
46
|
+
const token = Object.freeze({ modelKey, attemptId });
|
|
47
|
+
issuedAttemptTokens.add(token);
|
|
48
|
+
return token;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function assertManagedAttempt(
|
|
52
|
+
options: { fallbackManaged?: boolean; fallbackAttempt?: FallbackAttemptToken } | undefined,
|
|
53
|
+
): void {
|
|
54
|
+
if (!options?.fallbackManaged) return;
|
|
55
|
+
const token = options.fallbackAttempt;
|
|
56
|
+
if (!token || !issuedAttemptTokens.has(token)) {
|
|
57
|
+
throw new Error("fallbackManaged transport invocation requires a token returned by beginAttempt()");
|
|
58
|
+
}
|
|
59
|
+
if (consumedAttemptTokens.has(token)) {
|
|
60
|
+
throw new Error("fallbackManaged transport invocation cannot reuse a beginAttempt() token");
|
|
61
|
+
}
|
|
62
|
+
consumedAttemptTokens.add(token);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compatibility input for callers that have not yet wrapped their HTTP facts
|
|
67
|
+
* in the discriminated form. Only its structured fields are inspected.
|
|
68
|
+
*/
|
|
69
|
+
export interface FallbackTriggerInput {
|
|
70
|
+
status?: number;
|
|
71
|
+
providerCode?: string;
|
|
72
|
+
code?: string;
|
|
73
|
+
headers?: TransportHeaders;
|
|
74
|
+
response?: { status?: number; headers?: TransportHeaders };
|
|
75
|
+
error?: { code?: string; type?: string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isTransportHeaders(value: unknown): value is TransportHeaders {
|
|
79
|
+
try {
|
|
80
|
+
return value instanceof Headers || (!!value && typeof value === "object");
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function propertyOf(value: unknown, name: string): unknown {
|
|
87
|
+
if (!value || typeof value !== "object") return undefined;
|
|
88
|
+
try {
|
|
89
|
+
return Reflect.get(value, name);
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function finiteStatus(value: unknown): number | undefined {
|
|
96
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stringValue(value: unknown): string | undefined {
|
|
100
|
+
return typeof value === "string" ? value : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Retry-signal headers retained on transport facts; everything else is dropped. */
|
|
104
|
+
const RETAINED_TRANSPORT_HEADERS = ["retry-after", "retry-after-ms"] as const;
|
|
105
|
+
|
|
106
|
+
const RETAINED_TRANSPORT_HEADER_SET: ReadonlySet<string> = new Set(RETAINED_TRANSPORT_HEADERS);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reduce transport headers to the retained retry-signal entries in a plain
|
|
110
|
+
* record, so facts stay structured-cloneable and JSON-serializable and never
|
|
111
|
+
* persist arbitrary response headers into session files.
|
|
112
|
+
*
|
|
113
|
+
* Exception-safe by contract: inspection uses only `Headers.get()` results
|
|
114
|
+
* that are primitive strings or own data-descriptor record entries. Any
|
|
115
|
+
* failure omits headers instead of throwing — status/providerCode facts
|
|
116
|
+
* extracted by the caller must survive a hostile headers object.
|
|
117
|
+
*/
|
|
118
|
+
function retainedHeaderRecord(headers: TransportHeaders | undefined): Record<string, string> | undefined {
|
|
119
|
+
if (headers === undefined) return undefined;
|
|
120
|
+
let record: Record<string, string> | undefined;
|
|
121
|
+
try {
|
|
122
|
+
if (headers instanceof Headers) {
|
|
123
|
+
for (const name of RETAINED_TRANSPORT_HEADERS) {
|
|
124
|
+
const value = headers.get(name);
|
|
125
|
+
if (typeof value !== "string") continue;
|
|
126
|
+
record ??= {};
|
|
127
|
+
record[name] = value;
|
|
128
|
+
}
|
|
129
|
+
return record;
|
|
130
|
+
}
|
|
131
|
+
for (const key of Object.keys(headers)) {
|
|
132
|
+
const descriptor = Object.getOwnPropertyDescriptor(headers, key);
|
|
133
|
+
if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") continue;
|
|
134
|
+
const name = key.toLowerCase();
|
|
135
|
+
if (!RETAINED_TRANSPORT_HEADER_SET.has(name)) continue;
|
|
136
|
+
record ??= {};
|
|
137
|
+
record[name] = descriptor.value;
|
|
138
|
+
}
|
|
139
|
+
return record;
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Extracts only explicit HTTP/transport metadata; it never parses error text. */
|
|
146
|
+
export function transportFailureFacts(
|
|
147
|
+
error: unknown,
|
|
148
|
+
capturedResponse?: { status?: number; headers?: TransportHeaders },
|
|
149
|
+
): TransportFailureFacts | undefined {
|
|
150
|
+
if (!error || typeof error !== "object") return undefined;
|
|
151
|
+
const value = error as FallbackTriggerInput & { kind?: unknown; type?: unknown };
|
|
152
|
+
const response = propertyOf(value, "response");
|
|
153
|
+
const nestedError = propertyOf(value, "error");
|
|
154
|
+
const status =
|
|
155
|
+
finiteStatus(propertyOf(value, "status")) ??
|
|
156
|
+
finiteStatus(propertyOf(response, "status")) ??
|
|
157
|
+
finiteStatus(propertyOf(capturedResponse, "status"));
|
|
158
|
+
const anthropicErrorType = stringValue(propertyOf(nestedError, "type")) ?? stringValue(propertyOf(value, "type"));
|
|
159
|
+
const openaiErrorCode =
|
|
160
|
+
stringValue(propertyOf(value, "openaiErrorCode")) ?? stringValue(propertyOf(nestedError, "code"));
|
|
161
|
+
const providerCode =
|
|
162
|
+
stringValue(propertyOf(value, "providerCode")) ??
|
|
163
|
+
openaiErrorCode ??
|
|
164
|
+
stringValue(propertyOf(value, "code")) ??
|
|
165
|
+
anthropicErrorType;
|
|
166
|
+
const errorHeaders = propertyOf(value, "headers");
|
|
167
|
+
const responseHeaders = propertyOf(response, "headers");
|
|
168
|
+
const capturedHeaders = propertyOf(capturedResponse, "headers");
|
|
169
|
+
const rawHeaders = isTransportHeaders(errorHeaders)
|
|
170
|
+
? errorHeaders
|
|
171
|
+
: isTransportHeaders(responseHeaders)
|
|
172
|
+
? responseHeaders
|
|
173
|
+
: isTransportHeaders(capturedHeaders)
|
|
174
|
+
? capturedHeaders
|
|
175
|
+
: undefined;
|
|
176
|
+
// Normalize BEFORE the existence gate so normalization is idempotent:
|
|
177
|
+
// facts built from an error whose headers carry no retained retry signal
|
|
178
|
+
// must not exist on the first pass and then vanish when re-normalized
|
|
179
|
+
// (consumers deliberately re-run transportFailureFacts on embedded facts).
|
|
180
|
+
const headers = retainedHeaderRecord(rawHeaders);
|
|
181
|
+
const normalizedCode = providerCode?.toLowerCase();
|
|
182
|
+
if (
|
|
183
|
+
status === undefined &&
|
|
184
|
+
headers === undefined &&
|
|
185
|
+
!isQuotaCode(normalizedCode) &&
|
|
186
|
+
!isAuthCode(normalizedCode) &&
|
|
187
|
+
!isRateLimitCode(normalizedCode) &&
|
|
188
|
+
!isContextOverflowCode(normalizedCode)
|
|
189
|
+
) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return { kind: "transport", status, providerCode, anthropicErrorType, openaiErrorCode, headers };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function headersOf(headers: TransportHeaders | undefined): Headers | undefined {
|
|
196
|
+
if (headers instanceof Headers) return headers;
|
|
197
|
+
return headers ? new Headers(headers as Record<string, string>) : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseRetryAfterSeconds(value: string | null, now = Date.now()): number | undefined {
|
|
201
|
+
if (!value) return undefined;
|
|
202
|
+
const seconds = Number(value);
|
|
203
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);
|
|
204
|
+
const date = Date.parse(value);
|
|
205
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseRetryAfterMilliseconds(value: string | null): number | undefined {
|
|
209
|
+
if (!value) return undefined;
|
|
210
|
+
const milliseconds = Number(value);
|
|
211
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 ? Math.round(milliseconds) : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isContextOverflowCode(code: string | undefined): boolean {
|
|
215
|
+
return code === "context_length_exceeded";
|
|
216
|
+
}
|
|
217
|
+
function isQuotaCode(code: string | undefined): boolean {
|
|
218
|
+
return (
|
|
219
|
+
code === "insufficient_quota" ||
|
|
220
|
+
code === "quota_exceeded" ||
|
|
221
|
+
code === "quota_exhausted" ||
|
|
222
|
+
code === "usage_limit_reached" ||
|
|
223
|
+
code === "usage_not_included" ||
|
|
224
|
+
code === "out_of_credits"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isAuthCode(code: string | undefined): boolean {
|
|
229
|
+
return (
|
|
230
|
+
code === "authentication_error" ||
|
|
231
|
+
code === "invalid_api_key" ||
|
|
232
|
+
code === "invalid_token" ||
|
|
233
|
+
code === "token_expired" ||
|
|
234
|
+
code === "unauthorized" ||
|
|
235
|
+
code === "forbidden"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isRateLimitCode(code: string | undefined): boolean {
|
|
240
|
+
return (
|
|
241
|
+
code === "rate_limit" ||
|
|
242
|
+
code === "rate_limit_error" ||
|
|
243
|
+
code === "rate_limit_exceeded" ||
|
|
244
|
+
code === "too_many_requests"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Classifies only typed upstream transport facts without consuming response bodies. */
|
|
249
|
+
export function classifyFallbackTrigger(
|
|
250
|
+
errorOrFacts: TransportFailureFacts | FallbackTriggerInput | unknown,
|
|
251
|
+
): FallbackTrigger {
|
|
252
|
+
const facts = transportFailureFacts(errorOrFacts);
|
|
253
|
+
if (!facts) return { class: "other" };
|
|
254
|
+
const headers = headersOf(facts.headers);
|
|
255
|
+
const retryAfterMs =
|
|
256
|
+
parseRetryAfterMilliseconds(headers?.get("retry-after-ms") ?? null) ??
|
|
257
|
+
parseRetryAfterSeconds(headers?.get("retry-after") ?? null);
|
|
258
|
+
const code = (facts.openaiErrorCode ?? facts.anthropicErrorType ?? facts.providerCode)?.toLowerCase();
|
|
259
|
+
const triggerClass: FallbackTriggerClass = isQuotaCode(code)
|
|
260
|
+
? "quota"
|
|
261
|
+
: facts.status === 401 || facts.status === 403 || isAuthCode(code)
|
|
262
|
+
? "auth"
|
|
263
|
+
: facts.status === 429 || isRateLimitCode(code)
|
|
264
|
+
? "rate_limit"
|
|
265
|
+
: facts.status !== undefined && facts.status >= 500 && facts.status <= 599
|
|
266
|
+
? "server"
|
|
267
|
+
: "other";
|
|
268
|
+
return retryAfterMs === undefined ? { class: triggerClass } : { class: triggerClass, retryAfterMs };
|
|
269
|
+
}
|
package/src/utils/overflow.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage } from "../types";
|
|
2
|
+
import type { TransportFailureFacts } from "./fallback-transport";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Regex patterns to detect context overflow errors from different providers.
|
|
@@ -119,48 +120,88 @@ const EMPTY_RESPONSE_USAGE_THRESHOLD = 5;
|
|
|
119
120
|
* @param contextWindow - Optional context window size for detecting silent overflow (z.ai)
|
|
120
121
|
* @returns true if the message indicates a context overflow
|
|
121
122
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Authoritatively classify a context overflow from the assistant result and
|
|
125
|
+
* normalized transport facts. Typed facts take precedence over provider prose:
|
|
126
|
+
* an explicit non-overflow transport failure cannot be upgraded by hostile or
|
|
127
|
+
* misleading error text.
|
|
128
|
+
*/
|
|
129
|
+
const OVERFLOW_PROVIDER_CODES = new Set(["context_length_exceeded", "request_too_large"]);
|
|
130
|
+
const NON_OVERFLOW_PROVIDER_CODES = new Set([
|
|
131
|
+
"invalid_request_error",
|
|
132
|
+
"authentication_error",
|
|
133
|
+
"invalid_api_key",
|
|
134
|
+
"invalid_token",
|
|
135
|
+
"token_expired",
|
|
136
|
+
"unauthorized",
|
|
137
|
+
"forbidden",
|
|
138
|
+
"insufficient_quota",
|
|
139
|
+
"quota_exceeded",
|
|
140
|
+
"quota_exhausted",
|
|
141
|
+
"usage_limit_reached",
|
|
142
|
+
"usage_not_included",
|
|
143
|
+
"out_of_credits",
|
|
144
|
+
"rate_limit",
|
|
145
|
+
"rate_limit_error",
|
|
146
|
+
"rate_limit_exceeded",
|
|
147
|
+
"too_many_requests",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
function transportCodes(transportFailure: TransportFailureFacts | undefined): string[] {
|
|
151
|
+
return [transportFailure?.openaiErrorCode, transportFailure?.anthropicErrorType, transportFailure?.providerCode]
|
|
152
|
+
.filter((code): code is string => typeof code === "string")
|
|
153
|
+
.map(code => code.toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hasTypedNonOverflowCode(transportFailure: TransportFailureFacts | undefined): boolean {
|
|
157
|
+
return transportCodes(transportFailure).some(code => NON_OVERFLOW_PROVIDER_CODES.has(code));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isTypedNoBodyOverflow(
|
|
161
|
+
message: AssistantMessage,
|
|
162
|
+
transportFailure: TransportFailureFacts | undefined,
|
|
163
|
+
): boolean {
|
|
164
|
+
if (transportFailure?.status !== 400 && transportFailure?.status !== 413) return false;
|
|
165
|
+
return !message.errorMessage || /\b4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage);
|
|
166
|
+
}
|
|
129
167
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
export function classifyContextOverflow(
|
|
169
|
+
message: AssistantMessage,
|
|
170
|
+
transportFailure?: TransportFailureFacts,
|
|
171
|
+
contextWindow?: number,
|
|
172
|
+
): boolean {
|
|
173
|
+
if (transportFailure?.status === 429) return false;
|
|
174
|
+
const typedCodes = transportCodes(transportFailure);
|
|
175
|
+
if (typedCodes.some(code => OVERFLOW_PROVIDER_CODES.has(code))) return true;
|
|
176
|
+
if (hasTypedNonOverflowCode(transportFailure)) return false;
|
|
177
|
+
if (isTypedNoBodyOverflow(message, transportFailure)) return true;
|
|
178
|
+
|
|
179
|
+
const errorMessage = message.errorMessage;
|
|
180
|
+
if (message.stopReason === "error" && errorMessage) {
|
|
181
|
+
if (OVERFLOW_PATTERNS.some(pattern => pattern.test(errorMessage))) return true;
|
|
182
|
+
if (/\b4(00|13)\s*(status code)?\s*\(no body\)/i.test(errorMessage)) return true;
|
|
138
183
|
}
|
|
139
184
|
|
|
140
|
-
// Case 2: Usage-based overflow (silent or provider-specific)
|
|
141
185
|
if (contextWindow) {
|
|
142
186
|
const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
143
|
-
if (inputTokens > contextWindow)
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
187
|
+
if (inputTokens > contextWindow) return true;
|
|
146
188
|
}
|
|
147
189
|
|
|
148
|
-
|
|
149
|
-
// Some proxies (e.g. LiteLLM) return a "successful" response (stopReason "stop")
|
|
150
|
-
// with empty content and a near-zero token count when the upstream model's
|
|
151
|
-
// context window is exceeded. This is distinct from silent overflow (Case 2),
|
|
152
|
-
// where the provider reports the real input token count. Here the proxy
|
|
153
|
-
// fabricates a bogus usage (input: 1, output: 1) that is far below any
|
|
154
|
-
// realistic turn, so we detect it heuristically.
|
|
155
|
-
if (
|
|
190
|
+
return (
|
|
156
191
|
message.stopReason === "stop" &&
|
|
157
192
|
message.content.length === 0 &&
|
|
158
193
|
message.usage.input + message.usage.output <= EMPTY_RESPONSE_USAGE_THRESHOLD
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
162
196
|
|
|
163
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Check if an assistant message represents a context overflow error.
|
|
199
|
+
*
|
|
200
|
+
* Callers with normalized transport facts should use {@link classifyContextOverflow}
|
|
201
|
+
* so typed provider codes take precedence over error prose.
|
|
202
|
+
*/
|
|
203
|
+
export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
|
204
|
+
return classifyContextOverflow(message, undefined, contextWindow);
|
|
164
205
|
}
|
|
165
206
|
|
|
166
207
|
/**
|
package/src/utils/retry.ts
CHANGED
|
@@ -34,9 +34,9 @@ const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
|
|
|
34
34
|
*/
|
|
35
35
|
export async function callWithCopilotModelRetry<T>(
|
|
36
36
|
fn: () => Promise<T>,
|
|
37
|
-
options: { provider: string; signal?: AbortSignal; retryBaseDelayMs?: number },
|
|
37
|
+
options: { provider: string; signal?: AbortSignal; retryBaseDelayMs?: number; fallbackManaged?: boolean },
|
|
38
38
|
): Promise<T> {
|
|
39
|
-
if (options.provider !== "github-copilot") return fn();
|
|
39
|
+
if (options.provider !== "github-copilot" || options.fallbackManaged) return fn();
|
|
40
40
|
|
|
41
41
|
let lastError: unknown;
|
|
42
42
|
const retryBaseDelayMs = options.retryBaseDelayMs ?? COPILOT_MODEL_RETRY_BASE_DELAY_MS;
|