praxis-agent 0.48.1 → 0.49.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/README.md +4 -2
- package/dist/application/top-level-agent-manager.js +3 -0
- package/dist/core/runtime.d.ts +3 -0
- package/dist/core/runtime.js +3 -0
- package/dist/providers/anthropic-compatible.d.ts +2 -0
- package/dist/providers/anthropic-compatible.js +161 -6
- package/dist/providers/codex-subscription.js +11 -3
- package/dist/providers/deadline-provider.d.ts +8 -0
- package/dist/providers/deadline-provider.js +102 -10
- package/dist/providers/environment.d.ts +3 -0
- package/dist/providers/environment.js +21 -1
- package/dist/providers/non-streaming-fallback-provider.d.ts +17 -0
- package/dist/providers/non-streaming-fallback-provider.js +107 -0
- package/dist/providers/openai-compatible.js +7 -0
- package/dist/providers/provider-registry.js +24 -4
- package/dist/providers/provider-transport-activity.d.ts +7 -0
- package/dist/providers/provider-transport-activity.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,8 +215,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
215
215
|
disconnect recovery that never replays an already-dispatched call.
|
|
216
216
|
- **Provider-neutral models** — native Provider Registry/Vault routing, API
|
|
217
217
|
adapters, an experimental Codex OAuth adapter, explicit capability checks,
|
|
218
|
-
per-attempt
|
|
219
|
-
arguments without tool execution or
|
|
218
|
+
separate per-attempt connect, byte-idle, and absolute-total timeouts, typed
|
|
219
|
+
recovery for malformed streamed tool arguments without tool execution or
|
|
220
|
+
lost resumability, one default-on bounded Anthropic non-streaming replay for
|
|
221
|
+
eligible stream/idle failures without exposing failed-attempt output, and
|
|
220
222
|
token-only/no-API-dollar accounting for subscription runs.
|
|
221
223
|
- **Transactional self-update** — `praxis update` verifies the package before
|
|
222
224
|
installing it, rejects concurrent updates, and can roll back after an
|
|
@@ -29,10 +29,13 @@ const WORKER_RUNTIME_ENVIRONMENT = [
|
|
|
29
29
|
'PRAXIS_PROVIDER',
|
|
30
30
|
'PRAXIS_PROVIDER_PROFILE',
|
|
31
31
|
'PRAXIS_PROVIDER_DEADLINE_MS',
|
|
32
|
+
'PRAXIS_PROVIDER_CONNECT_TIMEOUT_MS',
|
|
33
|
+
'PRAXIS_PROVIDER_IDLE_TIMEOUT_MS',
|
|
32
34
|
'PRAXIS_BASE_URL',
|
|
33
35
|
'PRAXIS_MAX_OUTPUT_TOKENS',
|
|
34
36
|
'PRAXIS_ANTHROPIC_VERSION',
|
|
35
37
|
'PRAXIS_ANTHROPIC_WEB_SEARCH',
|
|
38
|
+
'PRAXIS_DISABLE_NONSTREAMING_FALLBACK',
|
|
36
39
|
'PRAXIS_ANTHROPIC_PROMPT_CACHING',
|
|
37
40
|
'PRAXIS_ANTHROPIC_PROMPT_CACHE_TTL',
|
|
38
41
|
'PRAXIS_CONTEXT_WINDOW_TOKENS',
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -537,14 +537,17 @@ export declare class ModelProviderError extends Error {
|
|
|
537
537
|
readonly status?: number;
|
|
538
538
|
readonly retryDelayMs?: number;
|
|
539
539
|
readonly kind?: ProviderErrorKind;
|
|
540
|
+
readonly timeoutPhase?: ProviderTimeoutPhase;
|
|
540
541
|
constructor(message: string, options: {
|
|
541
542
|
retryable: boolean;
|
|
542
543
|
kind?: ProviderErrorKind;
|
|
543
544
|
status?: number;
|
|
544
545
|
retryDelayMs?: number;
|
|
546
|
+
timeoutPhase?: ProviderTimeoutPhase;
|
|
545
547
|
cause?: unknown;
|
|
546
548
|
});
|
|
547
549
|
}
|
|
550
|
+
export type ProviderTimeoutPhase = 'connect' | 'idle' | 'total';
|
|
548
551
|
export declare class AgentRunCancelledError extends Error {
|
|
549
552
|
readonly name = "AgentRunCancelledError";
|
|
550
553
|
constructor();
|
package/dist/core/runtime.js
CHANGED
|
@@ -35,6 +35,7 @@ export class ModelProviderError extends Error {
|
|
|
35
35
|
status;
|
|
36
36
|
retryDelayMs;
|
|
37
37
|
kind;
|
|
38
|
+
timeoutPhase;
|
|
38
39
|
constructor(message, options) {
|
|
39
40
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
40
41
|
this.retryable = options.retryable;
|
|
@@ -44,6 +45,8 @@ export class ModelProviderError extends Error {
|
|
|
44
45
|
this.status = options.status;
|
|
45
46
|
if (options.retryDelayMs !== undefined)
|
|
46
47
|
this.retryDelayMs = options.retryDelayMs;
|
|
48
|
+
if (options.timeoutPhase !== undefined)
|
|
49
|
+
this.timeoutPhase = options.timeoutPhase;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
export class AgentRunCancelledError extends Error {
|
|
@@ -16,6 +16,7 @@ export interface AnthropicCompatibleProviderOptions {
|
|
|
16
16
|
maxToolMetadataBytes?: number;
|
|
17
17
|
maxErrorBodyBytes?: number;
|
|
18
18
|
fetchImplementation?: typeof fetch;
|
|
19
|
+
streaming?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare class AnthropicCompatibleProvider implements ModelProvider {
|
|
21
22
|
private readonly options;
|
|
@@ -32,6 +33,7 @@ export declare class AnthropicCompatibleProvider implements ModelProvider {
|
|
|
32
33
|
private readonly maxErrorBodyBytes;
|
|
33
34
|
private readonly thinking;
|
|
34
35
|
private readonly promptCaching;
|
|
36
|
+
private readonly streaming;
|
|
35
37
|
constructor(options: AnthropicCompatibleProviderOptions);
|
|
36
38
|
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
37
39
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
|
|
2
2
|
import { transportFailureKind } from './provider-errors.js';
|
|
3
|
+
import { reportProviderTransportActivity } from './provider-transport-activity.js';
|
|
4
|
+
import { markNonStreamingFallbackEligible } from './non-streaming-fallback-provider.js';
|
|
3
5
|
import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
|
|
4
6
|
function isRecord(value) {
|
|
5
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
8
|
}
|
|
9
|
+
function readNonNegativeTokenCount(usage, field, required) {
|
|
10
|
+
const value = usage[field];
|
|
11
|
+
if (value === undefined && !required)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
14
|
+
throw new ModelProviderError(`Provider returned an invalid ${field} counter`, { retryable: false });
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
7
18
|
function webSearchLinks(value) {
|
|
8
19
|
if (!Array.isArray(value))
|
|
9
20
|
return [];
|
|
@@ -633,6 +644,7 @@ export class AnthropicCompatibleProvider {
|
|
|
633
644
|
maxErrorBodyBytes;
|
|
634
645
|
thinking;
|
|
635
646
|
promptCaching;
|
|
647
|
+
streaming;
|
|
636
648
|
constructor(options) {
|
|
637
649
|
this.options = options;
|
|
638
650
|
if (options.contextWindowTokens !== undefined) {
|
|
@@ -640,6 +652,7 @@ export class AnthropicCompatibleProvider {
|
|
|
640
652
|
}
|
|
641
653
|
this.endpoint = `${options.baseUrl.replace(/\/+$/, '')}/messages`;
|
|
642
654
|
this.model = options.model;
|
|
655
|
+
this.streaming = options.streaming ?? true;
|
|
643
656
|
this.fetchImplementation = options.fetchImplementation ?? fetch;
|
|
644
657
|
this.maxOutputTokens = positiveInteger(options.maxOutputTokens ??
|
|
645
658
|
(options.model.includes('claude-opus-4-6')
|
|
@@ -648,7 +661,7 @@ export class AnthropicCompatibleProvider {
|
|
|
648
661
|
? 32_000
|
|
649
662
|
: 8192), 'Max output tokens');
|
|
650
663
|
this.capabilities = {
|
|
651
|
-
streaming:
|
|
664
|
+
streaming: this.streaming,
|
|
652
665
|
usage: true,
|
|
653
666
|
tools: true,
|
|
654
667
|
images: true,
|
|
@@ -720,7 +733,7 @@ export class AnthropicCompatibleProvider {
|
|
|
720
733
|
model: this.options.model,
|
|
721
734
|
max_tokens: maxTokens,
|
|
722
735
|
messages: serialized.messages,
|
|
723
|
-
stream:
|
|
736
|
+
stream: this.streaming,
|
|
724
737
|
...(thinkingPayload ? { thinking: thinkingPayload } : {}),
|
|
725
738
|
...(request.effort
|
|
726
739
|
? { output_config: { effort: request.effort } }
|
|
@@ -761,7 +774,9 @@ export class AnthropicCompatibleProvider {
|
|
|
761
774
|
requestInit.signal = request.signal;
|
|
762
775
|
let response;
|
|
763
776
|
try {
|
|
777
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
764
778
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
779
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
765
780
|
}
|
|
766
781
|
catch (error) {
|
|
767
782
|
const kind = transportFailureKind(error, request.signal);
|
|
@@ -792,6 +807,8 @@ export class AnthropicCompatibleProvider {
|
|
|
792
807
|
ended = true;
|
|
793
808
|
break;
|
|
794
809
|
}
|
|
810
|
+
if (value.byteLength > 0)
|
|
811
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
795
812
|
size += value.byteLength;
|
|
796
813
|
if (size > this.maxErrorBodyBytes) {
|
|
797
814
|
throw new ModelProviderError(`Provider error response exceeded ${this.maxErrorBodyBytes} bytes`, { retryable: false, status: response.status });
|
|
@@ -824,11 +841,144 @@ export class AnthropicCompatibleProvider {
|
|
|
824
841
|
status: response.status,
|
|
825
842
|
});
|
|
826
843
|
}
|
|
844
|
+
if (!this.streaming) {
|
|
845
|
+
if (!response.body) {
|
|
846
|
+
throw new ModelProviderError('Provider response has no body', {
|
|
847
|
+
kind: 'transport_error',
|
|
848
|
+
retryable: true,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const reader = response.body.getReader();
|
|
852
|
+
const chunks = [];
|
|
853
|
+
let size = 0;
|
|
854
|
+
let ended = false;
|
|
855
|
+
try {
|
|
856
|
+
while (true) {
|
|
857
|
+
const { done, value } = await reader.read();
|
|
858
|
+
if (done) {
|
|
859
|
+
ended = true;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
if (value.byteLength > 0)
|
|
863
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
864
|
+
size += value.byteLength;
|
|
865
|
+
if (size > this.maxStreamBufferBytes) {
|
|
866
|
+
throw new ModelProviderError(`Provider stream buffer exceeded ${this.maxStreamBufferBytes} bytes`, { retryable: false });
|
|
867
|
+
}
|
|
868
|
+
if (value.byteLength > 0)
|
|
869
|
+
chunks.push(value);
|
|
870
|
+
}
|
|
871
|
+
let payload;
|
|
872
|
+
try {
|
|
873
|
+
payload = JSON.parse(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8'));
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
throw new ModelProviderError('Provider returned malformed JSON', {
|
|
877
|
+
retryable: false,
|
|
878
|
+
cause: error,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (!isRecord(payload) ||
|
|
882
|
+
payload.type !== 'message' ||
|
|
883
|
+
payload.role !== 'assistant' ||
|
|
884
|
+
!Array.isArray(payload.content) ||
|
|
885
|
+
!isRecord(payload.usage)) {
|
|
886
|
+
throw new ModelProviderError('Provider returned an invalid message', {
|
|
887
|
+
retryable: false,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const state = {
|
|
891
|
+
blocks: new Map(),
|
|
892
|
+
thinking: new Map(),
|
|
893
|
+
tools: new Map(),
|
|
894
|
+
toolCallsSeen: 0,
|
|
895
|
+
metadataBytes: 0,
|
|
896
|
+
inputTokens: 0,
|
|
897
|
+
cacheReadInputTokens: 0,
|
|
898
|
+
cacheCreationInputTokens: 0,
|
|
899
|
+
outputTokens: 0,
|
|
900
|
+
webSearchRequests: 0,
|
|
901
|
+
usageSeen: false,
|
|
902
|
+
messageStarted: false,
|
|
903
|
+
messageDeltaSeen: false,
|
|
904
|
+
terminal: false,
|
|
905
|
+
};
|
|
906
|
+
const inputTokens = readNonNegativeTokenCount(payload.usage, 'input_tokens', true);
|
|
907
|
+
const outputTokens = readNonNegativeTokenCount(payload.usage, 'output_tokens', true);
|
|
908
|
+
const cacheReadInputTokens = readNonNegativeTokenCount(payload.usage, 'cache_read_input_tokens', false);
|
|
909
|
+
const cacheCreationInputTokens = readNonNegativeTokenCount(payload.usage, 'cache_creation_input_tokens', false);
|
|
910
|
+
const usage = {
|
|
911
|
+
input_tokens: inputTokens,
|
|
912
|
+
output_tokens: outputTokens,
|
|
913
|
+
...(cacheReadInputTokens === undefined
|
|
914
|
+
? {}
|
|
915
|
+
: { cache_read_input_tokens: cacheReadInputTokens }),
|
|
916
|
+
...(cacheCreationInputTokens === undefined
|
|
917
|
+
? {}
|
|
918
|
+
: { cache_creation_input_tokens: cacheCreationInputTokens }),
|
|
919
|
+
...(payload.usage.server_tool_use === undefined
|
|
920
|
+
? {}
|
|
921
|
+
: { server_tool_use: payload.usage.server_tool_use }),
|
|
922
|
+
};
|
|
923
|
+
const synthetic = [
|
|
924
|
+
{
|
|
925
|
+
type: 'message_start',
|
|
926
|
+
message: { usage },
|
|
927
|
+
},
|
|
928
|
+
...payload.content.flatMap((block, index) => {
|
|
929
|
+
if (!isRecord(block) || typeof block.type !== 'string') {
|
|
930
|
+
throw new ModelProviderError('Provider returned an invalid content block', { retryable: false });
|
|
931
|
+
}
|
|
932
|
+
return [
|
|
933
|
+
{ type: 'content_block_start', index, content_block: block },
|
|
934
|
+
{ type: 'content_block_stop', index },
|
|
935
|
+
];
|
|
936
|
+
}),
|
|
937
|
+
{
|
|
938
|
+
type: 'message_delta',
|
|
939
|
+
delta: { stop_reason: payload.stop_reason },
|
|
940
|
+
usage: {
|
|
941
|
+
output_tokens: outputTokens,
|
|
942
|
+
...(usage.server_tool_use === undefined
|
|
943
|
+
? {}
|
|
944
|
+
: { server_tool_use: usage.server_tool_use }),
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
{ type: 'message_stop' },
|
|
948
|
+
];
|
|
949
|
+
for (const event of synthetic) {
|
|
950
|
+
for (const normalized of parseSseEvent(JSON.stringify(event), state, this.maxToolArgumentsBytes, this.maxToolCallsPerResponse, this.maxToolMetadataBytes))
|
|
951
|
+
yield normalized;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
if (error instanceof ModelProviderError)
|
|
956
|
+
throw error;
|
|
957
|
+
const kind = transportFailureKind(error, request.signal);
|
|
958
|
+
throw new ModelProviderError('Provider response failed', {
|
|
959
|
+
kind,
|
|
960
|
+
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
961
|
+
cause: error,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
if (!ended) {
|
|
966
|
+
try {
|
|
967
|
+
await reader.cancel();
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
// Preserve the primary provider error.
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
reader.releaseLock();
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
827
977
|
if (!response.body) {
|
|
828
|
-
throw new ModelProviderError('Provider response has no body', {
|
|
978
|
+
throw markNonStreamingFallbackEligible(new ModelProviderError('Provider response has no body', {
|
|
829
979
|
kind: 'transport_error',
|
|
830
980
|
retryable: true,
|
|
831
|
-
});
|
|
981
|
+
}));
|
|
832
982
|
}
|
|
833
983
|
const reader = response.body.getReader();
|
|
834
984
|
const decoder = new TextDecoder();
|
|
@@ -853,6 +1003,8 @@ export class AnthropicCompatibleProvider {
|
|
|
853
1003
|
try {
|
|
854
1004
|
stream: while (true) {
|
|
855
1005
|
const { done, value } = await reader.read();
|
|
1006
|
+
if (!done && value.byteLength > 0)
|
|
1007
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
856
1008
|
buffer += decoder.decode(value, { stream: !done });
|
|
857
1009
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
858
1010
|
if (Buffer.byteLength(buffer) > this.maxStreamBufferBytes) {
|
|
@@ -882,18 +1034,21 @@ export class AnthropicCompatibleProvider {
|
|
|
882
1034
|
}
|
|
883
1035
|
}
|
|
884
1036
|
if (!state.terminal) {
|
|
885
|
-
throw new ModelProviderError('Provider stream ended before a terminal event', { retryable: true });
|
|
1037
|
+
throw markNonStreamingFallbackEligible(new ModelProviderError('Provider stream ended before a terminal event', { retryable: true }));
|
|
886
1038
|
}
|
|
887
1039
|
}
|
|
888
1040
|
catch (error) {
|
|
889
1041
|
if (error instanceof ModelProviderError)
|
|
890
1042
|
throw error;
|
|
891
1043
|
const kind = transportFailureKind(error, request.signal);
|
|
892
|
-
|
|
1044
|
+
const streamError = new ModelProviderError('Provider stream failed', {
|
|
893
1045
|
kind,
|
|
894
1046
|
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
895
1047
|
cause: error,
|
|
896
1048
|
});
|
|
1049
|
+
throw kind === 'timeout' || kind === 'transport_error'
|
|
1050
|
+
? markNonStreamingFallbackEligible(streamError)
|
|
1051
|
+
: streamError;
|
|
897
1052
|
}
|
|
898
1053
|
finally {
|
|
899
1054
|
if (!streamEnded) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
|
|
2
2
|
import { CodexOAuthError, } from './codex-oauth.js';
|
|
3
|
+
import { reportProviderTransportActivity } from './provider-transport-activity.js';
|
|
3
4
|
export const CODEX_RESPONSES_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
|
|
4
5
|
const DEFAULT_MAX_STREAM = 1024 * 1024;
|
|
5
6
|
const DEFAULT_MAX_ARGUMENTS = 1024 * 1024;
|
|
@@ -657,7 +658,7 @@ export function parseCodexSseFrame(data) {
|
|
|
657
658
|
reasoning: DEFAULT_MAX_REASONING,
|
|
658
659
|
});
|
|
659
660
|
}
|
|
660
|
-
async function drainBody(response, maxBytes) {
|
|
661
|
+
async function drainBody(response, maxBytes, request) {
|
|
661
662
|
const reader = response.body?.getReader();
|
|
662
663
|
if (!reader)
|
|
663
664
|
return;
|
|
@@ -670,6 +671,8 @@ async function drainBody(response, maxBytes) {
|
|
|
670
671
|
ended = true;
|
|
671
672
|
return;
|
|
672
673
|
}
|
|
674
|
+
if (next.value.byteLength > 0)
|
|
675
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
673
676
|
total += next.value.byteLength;
|
|
674
677
|
if (total > maxBytes)
|
|
675
678
|
return;
|
|
@@ -741,12 +744,14 @@ export class CodexSubscriptionProvider {
|
|
|
741
744
|
'content-type': 'application/json',
|
|
742
745
|
};
|
|
743
746
|
try {
|
|
747
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
744
748
|
response = await this.fetchImplementation(CODEX_RESPONSES_ENDPOINT, {
|
|
745
749
|
method: 'POST',
|
|
746
750
|
headers,
|
|
747
751
|
body: JSON.stringify(body),
|
|
748
752
|
...(signal ? { signal } : {}),
|
|
749
753
|
});
|
|
754
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
750
755
|
}
|
|
751
756
|
catch (error) {
|
|
752
757
|
throw transportError(error, signal);
|
|
@@ -754,12 +759,13 @@ export class CodexSubscriptionProvider {
|
|
|
754
759
|
if (response.status === 401 && !refreshed) {
|
|
755
760
|
refreshed = true;
|
|
756
761
|
try {
|
|
757
|
-
await drainBody(response, this.maxErrorBodyBytes);
|
|
762
|
+
await drainBody(response, this.maxErrorBodyBytes, request);
|
|
758
763
|
}
|
|
759
764
|
catch {
|
|
760
765
|
/* ignore unread body failures */
|
|
761
766
|
}
|
|
762
767
|
try {
|
|
768
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
763
769
|
access = await this.options.access({
|
|
764
770
|
forceAfter: access.accessToken,
|
|
765
771
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -772,7 +778,7 @@ export class CodexSubscriptionProvider {
|
|
|
772
778
|
}
|
|
773
779
|
if (!response.ok) {
|
|
774
780
|
try {
|
|
775
|
-
await drainBody(response, this.maxErrorBodyBytes);
|
|
781
|
+
await drainBody(response, this.maxErrorBodyBytes, request);
|
|
776
782
|
}
|
|
777
783
|
catch {
|
|
778
784
|
/* preserve generic error */
|
|
@@ -809,6 +815,8 @@ export class CodexSubscriptionProvider {
|
|
|
809
815
|
try {
|
|
810
816
|
while (true) {
|
|
811
817
|
const next = await reader.read();
|
|
818
|
+
if (!next.done && next.value.byteLength > 0)
|
|
819
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
812
820
|
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
813
821
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
814
822
|
if (bytes(buffer) > this.maxStreamBufferBytes)
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { type ModelProvider, type ModelRequest, type ModelStreamEvent } from '../core/runtime.js';
|
|
2
2
|
export declare const DEFAULT_PROVIDER_DEADLINE_MS = 90000;
|
|
3
|
+
export declare const DEFAULT_PROVIDER_CONNECT_TIMEOUT_MS = 90000;
|
|
4
|
+
export declare const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 90000;
|
|
3
5
|
export interface DeadlineModelProviderOptions {
|
|
4
6
|
provider: ModelProvider;
|
|
5
7
|
deadlineMs?: number;
|
|
8
|
+
connectTimeoutMs?: number;
|
|
9
|
+
idleTimeoutMs?: number;
|
|
6
10
|
}
|
|
7
11
|
/** Bounds each streamed completion while preserving the wrapped provider's API. */
|
|
8
12
|
export declare class DeadlineModelProvider implements ModelProvider {
|
|
9
13
|
private readonly provider;
|
|
10
14
|
private readonly deadlineMs;
|
|
15
|
+
private readonly connectTimeoutMs;
|
|
16
|
+
private readonly idleTimeoutMs;
|
|
17
|
+
private readonly connectTimeoutExplicit;
|
|
18
|
+
private readonly idleTimeoutExplicit;
|
|
11
19
|
readonly model?: string;
|
|
12
20
|
constructor(options: DeadlineModelProviderOptions);
|
|
13
21
|
get capabilities(): ModelProvider['capabilities'];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
import { detachProviderTransportActivity, observeProviderTransportActivity, } from './provider-transport-activity.js';
|
|
2
3
|
export const DEFAULT_PROVIDER_DEADLINE_MS = 90_000;
|
|
4
|
+
export const DEFAULT_PROVIDER_CONNECT_TIMEOUT_MS = 90_000;
|
|
5
|
+
export const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 90_000;
|
|
3
6
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
4
7
|
function deferred() {
|
|
5
8
|
let resolve;
|
|
@@ -12,6 +15,10 @@ function deferred() {
|
|
|
12
15
|
export class DeadlineModelProvider {
|
|
13
16
|
provider;
|
|
14
17
|
deadlineMs;
|
|
18
|
+
connectTimeoutMs;
|
|
19
|
+
idleTimeoutMs;
|
|
20
|
+
connectTimeoutExplicit;
|
|
21
|
+
idleTimeoutExplicit;
|
|
15
22
|
constructor(options) {
|
|
16
23
|
const deadlineMs = options.deadlineMs === undefined
|
|
17
24
|
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
@@ -21,8 +28,22 @@ export class DeadlineModelProvider {
|
|
|
21
28
|
deadlineMs <= 0) {
|
|
22
29
|
throw new Error('Provider deadline must be a positive integer');
|
|
23
30
|
}
|
|
31
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? deadlineMs;
|
|
32
|
+
if (!Number.isFinite(connectTimeoutMs) ||
|
|
33
|
+
!Number.isInteger(connectTimeoutMs) ||
|
|
34
|
+
connectTimeoutMs <= 0)
|
|
35
|
+
throw new Error('Provider connect timeout must be a positive integer');
|
|
36
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? deadlineMs;
|
|
37
|
+
if (!Number.isFinite(idleTimeoutMs) ||
|
|
38
|
+
!Number.isInteger(idleTimeoutMs) ||
|
|
39
|
+
idleTimeoutMs <= 0)
|
|
40
|
+
throw new Error('Provider idle timeout must be a positive integer');
|
|
24
41
|
this.provider = options.provider;
|
|
25
42
|
this.deadlineMs = deadlineMs;
|
|
43
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
44
|
+
this.idleTimeoutMs = idleTimeoutMs;
|
|
45
|
+
this.connectTimeoutExplicit = options.connectTimeoutMs !== undefined;
|
|
46
|
+
this.idleTimeoutExplicit = options.idleTimeoutMs !== undefined;
|
|
26
47
|
Object.defineProperty(this, 'model', {
|
|
27
48
|
configurable: false,
|
|
28
49
|
enumerable: true,
|
|
@@ -40,15 +61,30 @@ export class DeadlineModelProvider {
|
|
|
40
61
|
let callerSignal;
|
|
41
62
|
let timer;
|
|
42
63
|
let deadlineStartedAt;
|
|
64
|
+
let totalDueAt;
|
|
65
|
+
let phaseDueAt;
|
|
66
|
+
let phase = 'connect';
|
|
67
|
+
let pullPending = false;
|
|
68
|
+
let hasPulled = false;
|
|
69
|
+
let instrumentedRequest;
|
|
43
70
|
let underlying;
|
|
44
71
|
let cleanupStarted = false;
|
|
45
72
|
const interruptionDeferred = deferred();
|
|
46
73
|
const interruptionError = () => {
|
|
47
74
|
if (interruption?.kind === 'timeout') {
|
|
48
|
-
|
|
75
|
+
const timeoutPhase = interruption.cause instanceof TimeoutCause
|
|
76
|
+
? interruption.cause.phase
|
|
77
|
+
: undefined;
|
|
78
|
+
const message = timeoutPhase === 'connect'
|
|
79
|
+
? 'Provider connection timed out'
|
|
80
|
+
: timeoutPhase === 'idle'
|
|
81
|
+
? 'Provider stream idle timed out'
|
|
82
|
+
: 'Provider request timed out';
|
|
83
|
+
return new ModelProviderError(message, {
|
|
49
84
|
kind: 'timeout',
|
|
50
85
|
retryable: true,
|
|
51
86
|
cause: interruption.cause,
|
|
87
|
+
...(timeoutPhase === undefined ? {} : { timeoutPhase }),
|
|
52
88
|
});
|
|
53
89
|
}
|
|
54
90
|
return new ModelProviderError('Provider request cancelled', {
|
|
@@ -86,6 +122,8 @@ export class DeadlineModelProvider {
|
|
|
86
122
|
clearTimeout(timer);
|
|
87
123
|
timer = undefined;
|
|
88
124
|
callerSignal?.removeEventListener('abort', onCallerAbort);
|
|
125
|
+
if (instrumentedRequest)
|
|
126
|
+
detachProviderTransportActivity(instrumentedRequest);
|
|
89
127
|
};
|
|
90
128
|
const finish = (outcome) => {
|
|
91
129
|
if (interruption !== undefined)
|
|
@@ -99,19 +137,54 @@ export class DeadlineModelProvider {
|
|
|
99
137
|
bestEffortUnderlyingReturn();
|
|
100
138
|
};
|
|
101
139
|
const onCallerAbort = () => finish({ kind: 'cancelled', cause: callerSignal?.reason });
|
|
102
|
-
|
|
103
|
-
|
|
140
|
+
class TimeoutCause extends Error {
|
|
141
|
+
phase;
|
|
142
|
+
constructor(timeoutPhase) {
|
|
143
|
+
super('Provider request timed out');
|
|
144
|
+
this.name = 'TimeoutError';
|
|
145
|
+
this.phase = timeoutPhase;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
104
148
|
const scheduleDeadline = () => {
|
|
105
149
|
if (deadlineStartedAt === undefined || closed)
|
|
106
150
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
151
|
+
if (timer !== undefined)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
const now = performance.now();
|
|
154
|
+
const remainingTotal = (totalDueAt ?? now) - now;
|
|
155
|
+
const remainingPhase = pullPending
|
|
156
|
+
? (phaseDueAt ?? now) - now
|
|
157
|
+
: Number.POSITIVE_INFINITY;
|
|
158
|
+
const remainingMs = Math.min(remainingTotal, remainingPhase);
|
|
109
159
|
if (remainingMs <= 0) {
|
|
110
|
-
|
|
160
|
+
const phaseTimeoutExplicit = phase === 'connect'
|
|
161
|
+
? this.connectTimeoutExplicit
|
|
162
|
+
: this.idleTimeoutExplicit;
|
|
163
|
+
finish({
|
|
164
|
+
kind: 'timeout',
|
|
165
|
+
cause: new TimeoutCause(remainingPhase < remainingTotal ||
|
|
166
|
+
(remainingPhase <= remainingTotal && phaseTimeoutExplicit)
|
|
167
|
+
? phase
|
|
168
|
+
: 'total'),
|
|
169
|
+
});
|
|
111
170
|
return;
|
|
112
171
|
}
|
|
113
172
|
timer = setTimeout(scheduleDeadline, Math.min(remainingMs, MAX_TIMER_DELAY_MS));
|
|
114
173
|
};
|
|
174
|
+
const onTransportActivity = (reported) => {
|
|
175
|
+
if (closed || deadlineStartedAt === undefined)
|
|
176
|
+
return;
|
|
177
|
+
const now = performance.now();
|
|
178
|
+
if (reported === 'request-started') {
|
|
179
|
+
phase = 'connect';
|
|
180
|
+
phaseDueAt = now + this.connectTimeoutMs;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
phase = 'idle';
|
|
184
|
+
phaseDueAt = now + this.idleTimeoutMs;
|
|
185
|
+
}
|
|
186
|
+
scheduleDeadline();
|
|
187
|
+
};
|
|
115
188
|
const start = () => {
|
|
116
189
|
if (started || closed)
|
|
117
190
|
return;
|
|
@@ -125,11 +198,12 @@ export class DeadlineModelProvider {
|
|
|
125
198
|
if (interruption !== undefined)
|
|
126
199
|
return;
|
|
127
200
|
deadlineStartedAt = performance.now();
|
|
201
|
+
totalDueAt = deadlineStartedAt + this.deadlineMs;
|
|
202
|
+
phaseDueAt = deadlineStartedAt + this.connectTimeoutMs;
|
|
128
203
|
scheduleDeadline();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
204
|
+
instrumentedRequest = { ...request, signal: controller.signal };
|
|
205
|
+
observeProviderTransportActivity(instrumentedRequest, onTransportActivity);
|
|
206
|
+
const iterable = this.provider.complete(instrumentedRequest);
|
|
133
207
|
underlying = iterable[Symbol.asyncIterator]();
|
|
134
208
|
if (interruption !== undefined)
|
|
135
209
|
bestEffortUnderlyingReturn();
|
|
@@ -153,11 +227,23 @@ export class DeadlineModelProvider {
|
|
|
153
227
|
if (closed || !underlying)
|
|
154
228
|
return { done: true, value: undefined };
|
|
155
229
|
let nextPromise;
|
|
230
|
+
pullPending = true;
|
|
231
|
+
if (hasPulled) {
|
|
232
|
+
phaseDueAt =
|
|
233
|
+
performance.now() +
|
|
234
|
+
(phase === 'connect' ? this.connectTimeoutMs : this.idleTimeoutMs);
|
|
235
|
+
}
|
|
236
|
+
hasPulled = true;
|
|
237
|
+
scheduleDeadline();
|
|
238
|
+
if (interruption !== undefined)
|
|
239
|
+
throw interruptionError();
|
|
156
240
|
try {
|
|
157
241
|
const result = underlying.next();
|
|
158
242
|
nextPromise = Promise.resolve(result);
|
|
159
243
|
}
|
|
160
244
|
catch (error) {
|
|
245
|
+
pullPending = false;
|
|
246
|
+
scheduleDeadline();
|
|
161
247
|
if (interruption !== undefined)
|
|
162
248
|
throw interruptionError();
|
|
163
249
|
closed = true;
|
|
@@ -176,6 +262,8 @@ export class DeadlineModelProvider {
|
|
|
176
262
|
outcome,
|
|
177
263
|
})),
|
|
178
264
|
]);
|
|
265
|
+
pullPending = false;
|
|
266
|
+
scheduleDeadline();
|
|
179
267
|
const observedInterruption = interruption ??
|
|
180
268
|
(result.type === 'interruption' ? result.outcome : undefined);
|
|
181
269
|
if (observedInterruption !== undefined) {
|
|
@@ -200,6 +288,10 @@ export class DeadlineModelProvider {
|
|
|
200
288
|
closed = true;
|
|
201
289
|
clearResources();
|
|
202
290
|
}
|
|
291
|
+
else {
|
|
292
|
+
phase = 'idle';
|
|
293
|
+
phaseDueAt = performance.now() + this.idleTimeoutMs;
|
|
294
|
+
}
|
|
203
295
|
return result.value;
|
|
204
296
|
};
|
|
205
297
|
const returnFromConsumer = async () => {
|
|
@@ -2,9 +2,12 @@ export interface ProviderEnvironment {
|
|
|
2
2
|
provider: 'openai' | 'anthropic';
|
|
3
3
|
baseUrl: string;
|
|
4
4
|
deadlineMs: number;
|
|
5
|
+
connectTimeoutMs: number;
|
|
6
|
+
idleTimeoutMs: number;
|
|
5
7
|
maxOutputTokens?: number;
|
|
6
8
|
anthropicVersion?: string;
|
|
7
9
|
webSearch?: boolean;
|
|
10
|
+
disableNonStreamingFallback: boolean;
|
|
8
11
|
}
|
|
9
12
|
export interface ContextEnvironment {
|
|
10
13
|
contextWindowTokens?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAnthropicPromptCachePolicyResolver } from './anthropic-prompt-cache.js';
|
|
2
|
-
import { DEFAULT_PROVIDER_DEADLINE_MS } from './deadline-provider.js';
|
|
2
|
+
import { DEFAULT_PROVIDER_CONNECT_TIMEOUT_MS, DEFAULT_PROVIDER_DEADLINE_MS, DEFAULT_PROVIDER_IDLE_TIMEOUT_MS, } from './deadline-provider.js';
|
|
3
3
|
export function parseProviderEnvironment(environment) {
|
|
4
4
|
const provider = environment.PRAXIS_PROVIDER ?? 'openai';
|
|
5
5
|
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
@@ -15,6 +15,17 @@ export function parseProviderEnvironment(environment) {
|
|
|
15
15
|
const deadlineMs = rawDeadlineMs === undefined
|
|
16
16
|
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
17
17
|
: Number(rawDeadlineMs);
|
|
18
|
+
const parseTimeout = (name, fallback) => {
|
|
19
|
+
const raw = environment[name];
|
|
20
|
+
if (raw !== undefined &&
|
|
21
|
+
(!/^\d+$/.test(raw) ||
|
|
22
|
+
Number(raw) <= 0 ||
|
|
23
|
+
!Number.isSafeInteger(Number(raw))))
|
|
24
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
25
|
+
return raw === undefined ? fallback : Number(raw);
|
|
26
|
+
};
|
|
27
|
+
const connectTimeoutMs = parseTimeout('PRAXIS_PROVIDER_CONNECT_TIMEOUT_MS', DEFAULT_PROVIDER_CONNECT_TIMEOUT_MS);
|
|
28
|
+
const idleTimeoutMs = parseTimeout('PRAXIS_PROVIDER_IDLE_TIMEOUT_MS', DEFAULT_PROVIDER_IDLE_TIMEOUT_MS);
|
|
18
29
|
const maxOutputTokens = environment.PRAXIS_MAX_OUTPUT_TOKENS;
|
|
19
30
|
if (maxOutputTokens !== undefined &&
|
|
20
31
|
(!/^\d+$/.test(maxOutputTokens) ||
|
|
@@ -38,6 +49,12 @@ export function parseProviderEnvironment(environment) {
|
|
|
38
49
|
webSearch !== 'false') {
|
|
39
50
|
throw new Error('PRAXIS_ANTHROPIC_WEB_SEARCH must be true or false');
|
|
40
51
|
}
|
|
52
|
+
const disableFallback = environment.PRAXIS_DISABLE_NONSTREAMING_FALLBACK;
|
|
53
|
+
if (disableFallback !== undefined &&
|
|
54
|
+
disableFallback !== 'true' &&
|
|
55
|
+
disableFallback !== 'false') {
|
|
56
|
+
throw new Error('PRAXIS_DISABLE_NONSTREAMING_FALLBACK must be true or false');
|
|
57
|
+
}
|
|
41
58
|
if (provider === 'openai' && webSearch !== undefined) {
|
|
42
59
|
throw new Error('PRAXIS_ANTHROPIC_WEB_SEARCH requires PRAXIS_PROVIDER=anthropic');
|
|
43
60
|
}
|
|
@@ -59,11 +76,14 @@ export function parseProviderEnvironment(environment) {
|
|
|
59
76
|
? 'https://api.anthropic.com/v1'
|
|
60
77
|
: 'https://api.openai.com/v1'),
|
|
61
78
|
deadlineMs,
|
|
79
|
+
connectTimeoutMs,
|
|
80
|
+
idleTimeoutMs,
|
|
62
81
|
...(maxOutputTokens === undefined
|
|
63
82
|
? {}
|
|
64
83
|
: { maxOutputTokens: Number(maxOutputTokens) }),
|
|
65
84
|
...(anthropicVersion === undefined ? {} : { anthropicVersion }),
|
|
66
85
|
...(webSearch === undefined ? {} : { webSearch: webSearch === 'true' }),
|
|
86
|
+
disableNonStreamingFallback: disableFallback === 'true',
|
|
67
87
|
};
|
|
68
88
|
}
|
|
69
89
|
export function parseContextEnvironment(environment) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ModelProvider, type ModelRequest, type ModelStreamEvent } from '../core/runtime.js';
|
|
2
|
+
/** Marks a provider error as safe for the bounded non-streaming replay. */
|
|
3
|
+
export declare function markNonStreamingFallbackEligible<T extends Error>(error: T): T;
|
|
4
|
+
export declare function isNonStreamingFallbackEligible(error: unknown): boolean;
|
|
5
|
+
/** Replays an eligible failed streaming request through one non-streaming call. */
|
|
6
|
+
export declare class NonStreamingFallbackModelProvider implements ModelProvider {
|
|
7
|
+
private readonly provider;
|
|
8
|
+
private readonly nonStreamingProvider;
|
|
9
|
+
constructor(options: {
|
|
10
|
+
provider: ModelProvider;
|
|
11
|
+
nonStreamingProvider?: ModelProvider;
|
|
12
|
+
});
|
|
13
|
+
readonly model?: string;
|
|
14
|
+
get capabilities(): ModelProvider['capabilities'];
|
|
15
|
+
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=non-streaming-fallback-provider.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ModelProviderError, modelProviderErrorKind, } from '../core/runtime.js';
|
|
2
|
+
const eligibleErrors = new WeakSet();
|
|
3
|
+
/** Marks a provider error as safe for the bounded non-streaming replay. */
|
|
4
|
+
export function markNonStreamingFallbackEligible(error) {
|
|
5
|
+
eligibleErrors.add(error);
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
export function isNonStreamingFallbackEligible(error) {
|
|
9
|
+
return ((typeof error === 'object' &&
|
|
10
|
+
error !== null &&
|
|
11
|
+
eligibleErrors.has(error)) ||
|
|
12
|
+
(error instanceof ModelProviderError && error.timeoutPhase === 'idle'));
|
|
13
|
+
}
|
|
14
|
+
function cancellationError(signal) {
|
|
15
|
+
return new ModelProviderError('Provider request cancelled', {
|
|
16
|
+
kind: 'cancelled',
|
|
17
|
+
retryable: false,
|
|
18
|
+
cause: signal.reason,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function throwIfAborted(signal) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
throw cancellationError(signal);
|
|
24
|
+
}
|
|
25
|
+
/** Replays an eligible failed streaming request through one non-streaming call. */
|
|
26
|
+
export class NonStreamingFallbackModelProvider {
|
|
27
|
+
provider;
|
|
28
|
+
nonStreamingProvider;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.provider = options.provider;
|
|
31
|
+
this.nonStreamingProvider = options.nonStreamingProvider;
|
|
32
|
+
Object.defineProperty(this, 'model', {
|
|
33
|
+
configurable: false,
|
|
34
|
+
enumerable: true,
|
|
35
|
+
get: () => this.provider.model,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
get capabilities() {
|
|
39
|
+
return this.provider.capabilities;
|
|
40
|
+
}
|
|
41
|
+
async *complete(request) {
|
|
42
|
+
const primaryEvents = [];
|
|
43
|
+
let primaryTerminal = false;
|
|
44
|
+
try {
|
|
45
|
+
for await (const event of this.provider.complete(request)) {
|
|
46
|
+
if (primaryTerminal) {
|
|
47
|
+
throw new ModelProviderError(`Provider emitted ${event.type} after its terminal event`, { retryable: false });
|
|
48
|
+
}
|
|
49
|
+
if (event.type === 'terminal')
|
|
50
|
+
primaryTerminal = true;
|
|
51
|
+
primaryEvents.push(event);
|
|
52
|
+
}
|
|
53
|
+
if (this.provider.capabilities.terminalReasons === true &&
|
|
54
|
+
!primaryTerminal) {
|
|
55
|
+
throw new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true });
|
|
56
|
+
}
|
|
57
|
+
yield* primaryEvents;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throwIfAborted(request.signal);
|
|
62
|
+
if (!this.nonStreamingProvider ||
|
|
63
|
+
!isNonStreamingFallbackEligible(error)) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
throwIfAborted(request.signal);
|
|
67
|
+
if (error instanceof ModelProviderError) {
|
|
68
|
+
yield {
|
|
69
|
+
type: 'api-retry',
|
|
70
|
+
attempt: 1,
|
|
71
|
+
maxRetries: 1,
|
|
72
|
+
retryDelayMs: 0,
|
|
73
|
+
errorStatus: error.status ?? null,
|
|
74
|
+
error: modelProviderErrorKind(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// The retry event contract carries a typed provider error kind. An
|
|
79
|
+
// explicitly marked non-ModelProviderError is still replayable.
|
|
80
|
+
yield {
|
|
81
|
+
type: 'api-retry',
|
|
82
|
+
attempt: 1,
|
|
83
|
+
maxRetries: 1,
|
|
84
|
+
retryDelayMs: 0,
|
|
85
|
+
errorStatus: null,
|
|
86
|
+
error: 'server_error',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const fallbackEvents = [];
|
|
90
|
+
let fallbackTerminal = false;
|
|
91
|
+
for await (const event of this.nonStreamingProvider.complete(request)) {
|
|
92
|
+
if (fallbackTerminal) {
|
|
93
|
+
throw new ModelProviderError(`Provider emitted ${event.type} after its terminal event`, { retryable: false });
|
|
94
|
+
}
|
|
95
|
+
if (event.type === 'terminal')
|
|
96
|
+
fallbackTerminal = true;
|
|
97
|
+
fallbackEvents.push(event);
|
|
98
|
+
}
|
|
99
|
+
if (this.nonStreamingProvider.capabilities.terminalReasons === true &&
|
|
100
|
+
!fallbackTerminal) {
|
|
101
|
+
throw new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true });
|
|
102
|
+
}
|
|
103
|
+
yield* fallbackEvents;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=non-streaming-fallback-provider.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
|
|
2
2
|
import { transportFailureKind } from './provider-errors.js';
|
|
3
|
+
import { reportProviderTransportActivity } from './provider-transport-activity.js';
|
|
3
4
|
function isRecord(value) {
|
|
4
5
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
6
|
}
|
|
@@ -351,7 +352,9 @@ export class OpenAICompatibleProvider {
|
|
|
351
352
|
requestInit.signal = request.signal;
|
|
352
353
|
let response;
|
|
353
354
|
try {
|
|
355
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
354
356
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
357
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
355
358
|
}
|
|
356
359
|
catch (error) {
|
|
357
360
|
const kind = transportFailureKind(error, request.signal);
|
|
@@ -382,6 +385,8 @@ export class OpenAICompatibleProvider {
|
|
|
382
385
|
ended = true;
|
|
383
386
|
break;
|
|
384
387
|
}
|
|
388
|
+
if (value.byteLength > 0)
|
|
389
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
385
390
|
size += value.byteLength;
|
|
386
391
|
if (size > this.maxErrorBodyBytes) {
|
|
387
392
|
throw new ModelProviderError(`Provider error response exceeded ${this.maxErrorBodyBytes} bytes`, { retryable: false, status: response.status });
|
|
@@ -434,6 +439,8 @@ export class OpenAICompatibleProvider {
|
|
|
434
439
|
try {
|
|
435
440
|
stream: while (true) {
|
|
436
441
|
const { done, value } = await reader.read();
|
|
442
|
+
if (!done && value.byteLength > 0)
|
|
443
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
437
444
|
buffer += decoder.decode(value, { stream: !done });
|
|
438
445
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
439
446
|
if (Buffer.byteLength(buffer) > this.maxStreamBufferBytes) {
|
|
@@ -2,6 +2,7 @@ import { AnthropicCompatibleProvider } from './anthropic-compatible.js';
|
|
|
2
2
|
import { OpenAICompatibleProvider } from './openai-compatible.js';
|
|
3
3
|
import { CodexSubscriptionProvider } from './codex-subscription.js';
|
|
4
4
|
import { DeadlineModelProvider } from './deadline-provider.js';
|
|
5
|
+
import { NonStreamingFallbackModelProvider } from './non-streaming-fallback-provider.js';
|
|
5
6
|
import { CodexOAuthCredentialManager, } from './codex-oauth.js';
|
|
6
7
|
import { resolveProviderTarget, } from './provider-settings.js';
|
|
7
8
|
import { ProviderAuthenticationError, resolveProviderCredential, } from './provider-auth.js';
|
|
@@ -135,7 +136,7 @@ class NativeProviderRegistry {
|
|
|
135
136
|
if (this.options.credential.type !== 'api-key') {
|
|
136
137
|
throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: an API key is required');
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
const anthropicOptions = {
|
|
139
140
|
baseUrl: target.baseUrl,
|
|
140
141
|
model: target.modelId,
|
|
141
142
|
apiKey: this.options.credential.secret,
|
|
@@ -173,15 +174,34 @@ class NativeProviderRegistry {
|
|
|
173
174
|
...(this.options.fetchImplementation === undefined
|
|
174
175
|
? {}
|
|
175
176
|
: { fetchImplementation: this.options.fetchImplementation }),
|
|
177
|
+
};
|
|
178
|
+
const streaming = this.withDeadline(new AnthropicCompatibleProvider({
|
|
179
|
+
...anthropicOptions,
|
|
180
|
+
streaming: true,
|
|
176
181
|
}));
|
|
182
|
+
if (this.options.providerEnvironment?.disableNonStreamingFallback === true)
|
|
183
|
+
return streaming;
|
|
184
|
+
const nonStreaming = this.withDeadline(new AnthropicCompatibleProvider({
|
|
185
|
+
...anthropicOptions,
|
|
186
|
+
streaming: false,
|
|
187
|
+
}));
|
|
188
|
+
return new NonStreamingFallbackModelProvider({
|
|
189
|
+
provider: streaming,
|
|
190
|
+
nonStreamingProvider: nonStreaming,
|
|
191
|
+
});
|
|
177
192
|
}
|
|
178
193
|
throw new ProviderRegistryError('unsupported_provider', `Unsupported provider protocol: ${target.protocol}`);
|
|
179
194
|
}
|
|
180
195
|
withDeadline(provider) {
|
|
181
|
-
const
|
|
182
|
-
return
|
|
196
|
+
const environment = this.options.providerEnvironment;
|
|
197
|
+
return environment === undefined
|
|
183
198
|
? provider
|
|
184
|
-
: new DeadlineModelProvider({
|
|
199
|
+
: new DeadlineModelProvider({
|
|
200
|
+
provider,
|
|
201
|
+
deadlineMs: environment.deadlineMs,
|
|
202
|
+
connectTimeoutMs: environment.connectTimeoutMs,
|
|
203
|
+
idleTimeoutMs: environment.idleTimeoutMs,
|
|
204
|
+
});
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
207
|
//# sourceMappingURL=provider-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelRequest } from '../core/runtime.js';
|
|
2
|
+
export type ProviderTransportActivity = 'request-started' | 'response-received' | 'response-chunk';
|
|
3
|
+
export type ProviderTransportActivityObserver = (activity: ProviderTransportActivity) => void;
|
|
4
|
+
export declare function observeProviderTransportActivity(request: ModelRequest, observer: ProviderTransportActivityObserver): void;
|
|
5
|
+
export declare function reportProviderTransportActivity(request: ModelRequest, activity: ProviderTransportActivity): void;
|
|
6
|
+
export declare function detachProviderTransportActivity(request: ModelRequest): void;
|
|
7
|
+
//# sourceMappingURL=provider-transport-activity.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const observers = new WeakMap();
|
|
2
|
+
export function observeProviderTransportActivity(request, observer) {
|
|
3
|
+
observers.set(request, observer);
|
|
4
|
+
}
|
|
5
|
+
export function reportProviderTransportActivity(request, activity) {
|
|
6
|
+
observers.get(request)?.(activity);
|
|
7
|
+
}
|
|
8
|
+
export function detachProviderTransportActivity(request) {
|
|
9
|
+
observers.delete(request);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=provider-transport-activity.js.map
|