praxis-agent 0.48.0 → 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 +6 -3
- package/dist/application/session-service.js +6 -2
- package/dist/application/top-level-agent-manager.js +3 -0
- package/dist/core/runtime.d.ts +10 -0
- package/dist/core/runtime.js +24 -1
- package/dist/core/transcript-event.js +9 -2
- package/dist/providers/anthropic-compatible.d.ts +2 -0
- package/dist/providers/anthropic-compatible.js +172 -9
- package/dist/providers/codex-subscription.js +18 -5
- 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 +18 -3
- 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,11 @@ 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
|
-
|
|
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
|
|
222
|
+
token-only/no-API-dollar accounting for subscription runs.
|
|
220
223
|
- **Transactional self-update** — `praxis update` verifies the package before
|
|
221
224
|
installing it, rejects concurrent updates, and can roll back after an
|
|
222
225
|
interruption or crash.
|
|
@@ -301,7 +304,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
301
304
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
302
305
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
303
306
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
304
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
307
|
+
type-only modules). `npm run test:fixtures` executes the 69-behavior native contract; 61 behaviors
|
|
305
308
|
are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
|
|
306
309
|
performs the structural check and is part of `npm run check`.
|
|
307
310
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
@@ -15,7 +15,7 @@ import { projectNativeSessionEntries } from './native-session-projection.js';
|
|
|
15
15
|
import { classifyClaudeInterruption, } from '../native/interruption.js';
|
|
16
16
|
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../native/tool-links.js';
|
|
17
17
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../native/translation.js';
|
|
18
|
-
import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
|
|
18
|
+
import { AgentRunCancelledError, AgentRuntime, MALFORMED_TOOL_INPUT_MESSAGE, ModelProviderError, } from '../core/runtime.js';
|
|
19
19
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
20
20
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
21
21
|
import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
|
|
@@ -3347,7 +3347,11 @@ export class ClaudeSessionService {
|
|
|
3347
3347
|
: {}),
|
|
3348
3348
|
};
|
|
3349
3349
|
const unresolvedToolCall = unresolvedToolCalls[0];
|
|
3350
|
-
if (unresolvedToolCall &&
|
|
3350
|
+
if (unresolvedToolCall &&
|
|
3351
|
+
!unresolvedToolCalls.every((call) => call.inputError?.kind === 'malformed_json' &&
|
|
3352
|
+
call.inputError?.message ===
|
|
3353
|
+
MALFORMED_TOOL_INPUT_MESSAGE) &&
|
|
3354
|
+
!approveRecovery) {
|
|
3351
3355
|
throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
|
|
3352
3356
|
}
|
|
3353
3357
|
const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
|
|
@@ -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
|
@@ -43,7 +43,14 @@ export interface ModelToolCall {
|
|
|
43
43
|
id: string;
|
|
44
44
|
name: string;
|
|
45
45
|
input: Record<string, unknown>;
|
|
46
|
+
inputError?: ModelToolInputError;
|
|
46
47
|
}
|
|
48
|
+
export declare const MALFORMED_TOOL_INPUT_MESSAGE = "Malformed tool input";
|
|
49
|
+
export interface ModelToolInputError {
|
|
50
|
+
kind: 'malformed_json';
|
|
51
|
+
message: typeof MALFORMED_TOOL_INPUT_MESSAGE;
|
|
52
|
+
}
|
|
53
|
+
export declare function malformedModelToolCall(id: string, name: string): ModelToolCall;
|
|
47
54
|
export type ModelThinkingMode = 'enabled' | 'adaptive' | 'disabled';
|
|
48
55
|
export type ModelThinkingBlock = {
|
|
49
56
|
type: 'thinking';
|
|
@@ -530,14 +537,17 @@ export declare class ModelProviderError extends Error {
|
|
|
530
537
|
readonly status?: number;
|
|
531
538
|
readonly retryDelayMs?: number;
|
|
532
539
|
readonly kind?: ProviderErrorKind;
|
|
540
|
+
readonly timeoutPhase?: ProviderTimeoutPhase;
|
|
533
541
|
constructor(message: string, options: {
|
|
534
542
|
retryable: boolean;
|
|
535
543
|
kind?: ProviderErrorKind;
|
|
536
544
|
status?: number;
|
|
537
545
|
retryDelayMs?: number;
|
|
546
|
+
timeoutPhase?: ProviderTimeoutPhase;
|
|
538
547
|
cause?: unknown;
|
|
539
548
|
});
|
|
540
549
|
}
|
|
550
|
+
export type ProviderTimeoutPhase = 'connect' | 'idle' | 'total';
|
|
541
551
|
export declare class AgentRunCancelledError extends Error {
|
|
542
552
|
readonly name = "AgentRunCancelledError";
|
|
543
553
|
constructor();
|
package/dist/core/runtime.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { ToolExecutionScheduler } from './tool-execution-scheduler.js';
|
|
2
2
|
import { resolveToolSchedulingPolicy } from './tool-scheduling-policy.js';
|
|
3
|
+
export const MALFORMED_TOOL_INPUT_MESSAGE = 'Malformed tool input';
|
|
4
|
+
export function malformedModelToolCall(id, name) {
|
|
5
|
+
return {
|
|
6
|
+
id,
|
|
7
|
+
name,
|
|
8
|
+
input: {},
|
|
9
|
+
inputError: {
|
|
10
|
+
kind: 'malformed_json',
|
|
11
|
+
message: MALFORMED_TOOL_INPUT_MESSAGE,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
3
15
|
const permissionDecisionSources = new WeakMap();
|
|
4
16
|
const autoModePermissionOutcomes = new WeakMap();
|
|
5
17
|
export function annotatePermissionDecision(decision, source) {
|
|
@@ -23,6 +35,7 @@ export class ModelProviderError extends Error {
|
|
|
23
35
|
status;
|
|
24
36
|
retryDelayMs;
|
|
25
37
|
kind;
|
|
38
|
+
timeoutPhase;
|
|
26
39
|
constructor(message, options) {
|
|
27
40
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
28
41
|
this.retryable = options.retryable;
|
|
@@ -32,6 +45,8 @@ export class ModelProviderError extends Error {
|
|
|
32
45
|
this.status = options.status;
|
|
33
46
|
if (options.retryDelayMs !== undefined)
|
|
34
47
|
this.retryDelayMs = options.retryDelayMs;
|
|
48
|
+
if (options.timeoutPhase !== undefined)
|
|
49
|
+
this.timeoutPhase = options.timeoutPhase;
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
export class AgentRunCancelledError extends Error {
|
|
@@ -516,7 +531,12 @@ export class AgentRuntime {
|
|
|
516
531
|
}
|
|
517
532
|
toolCalls.push(event.call);
|
|
518
533
|
this.emit(event);
|
|
519
|
-
const policy =
|
|
534
|
+
const policy = event.call.inputError
|
|
535
|
+
? {
|
|
536
|
+
concurrency: 'exclusive',
|
|
537
|
+
startAfterAssistant: true,
|
|
538
|
+
}
|
|
539
|
+
: resolveToolSchedulingPolicy(this.options.tools, event.call);
|
|
520
540
|
toolScheduler.schedule(event.call, request.observer?.toolExecutionStarted
|
|
521
541
|
? { ...policy, startAfterAssistant: true }
|
|
522
542
|
: policy);
|
|
@@ -863,6 +883,9 @@ export class AgentRuntime {
|
|
|
863
883
|
try {
|
|
864
884
|
if (emitPresentation)
|
|
865
885
|
emitProgress(0);
|
|
886
|
+
if (call.inputError) {
|
|
887
|
+
return { content: call.inputError.message, isError: true };
|
|
888
|
+
}
|
|
866
889
|
const executed = await this.executeTool(call, { ...request, signal }, messages);
|
|
867
890
|
const unsupportedImages = executed.images?.length && this.provider.capabilities.images !== true;
|
|
868
891
|
const unsupportedDocuments = executed.documents?.length &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MALFORMED_TOOL_INPUT_MESSAGE } from './runtime.js';
|
|
1
2
|
const terminalReasons = new Set([
|
|
2
3
|
'end_turn',
|
|
3
4
|
'tool_use',
|
|
@@ -51,11 +52,17 @@ export function isModelContentBlock(value) {
|
|
|
51
52
|
return isModelMedia(value);
|
|
52
53
|
}
|
|
53
54
|
function isModelToolCall(value) {
|
|
55
|
+
const inputError = isRecord(value) ? value.inputError : undefined;
|
|
54
56
|
return (isRecord(value) &&
|
|
55
|
-
hasOnlyKeys(value, ['id', 'name', 'input']) &&
|
|
57
|
+
hasOnlyKeys(value, ['id', 'name', 'input', 'inputError']) &&
|
|
56
58
|
isNonEmptyString(value.id) &&
|
|
57
59
|
isNonEmptyString(value.name) &&
|
|
58
|
-
isRecord(value.input)
|
|
60
|
+
isRecord(value.input) &&
|
|
61
|
+
(inputError === undefined ||
|
|
62
|
+
(isRecord(inputError) &&
|
|
63
|
+
hasOnlyKeys(inputError, ['kind', 'message']) &&
|
|
64
|
+
inputError.kind === 'malformed_json' &&
|
|
65
|
+
inputError.message === MALFORMED_TOOL_INPUT_MESSAGE)));
|
|
59
66
|
}
|
|
60
67
|
function isModelThinkingBlock(value) {
|
|
61
68
|
return isRecord(value) && value.type === 'thinking'
|
|
@@ -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
|
-
import { ModelProviderError, } from '../core/runtime.js';
|
|
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 [];
|
|
@@ -119,8 +130,16 @@ function completedToolCall(state, index) {
|
|
|
119
130
|
try {
|
|
120
131
|
input = JSON.parse(pending.partialJson);
|
|
121
132
|
}
|
|
122
|
-
catch
|
|
123
|
-
|
|
133
|
+
catch {
|
|
134
|
+
if (!pending.id || !pending.name) {
|
|
135
|
+
throw new ModelProviderError('Provider returned an invalid tool call', {
|
|
136
|
+
retryable: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
type: 'tool-call',
|
|
141
|
+
call: malformedModelToolCall(pending.id, pending.name),
|
|
142
|
+
};
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
if (!pending.id || !pending.name || !isRecord(input)) {
|
|
@@ -625,6 +644,7 @@ export class AnthropicCompatibleProvider {
|
|
|
625
644
|
maxErrorBodyBytes;
|
|
626
645
|
thinking;
|
|
627
646
|
promptCaching;
|
|
647
|
+
streaming;
|
|
628
648
|
constructor(options) {
|
|
629
649
|
this.options = options;
|
|
630
650
|
if (options.contextWindowTokens !== undefined) {
|
|
@@ -632,6 +652,7 @@ export class AnthropicCompatibleProvider {
|
|
|
632
652
|
}
|
|
633
653
|
this.endpoint = `${options.baseUrl.replace(/\/+$/, '')}/messages`;
|
|
634
654
|
this.model = options.model;
|
|
655
|
+
this.streaming = options.streaming ?? true;
|
|
635
656
|
this.fetchImplementation = options.fetchImplementation ?? fetch;
|
|
636
657
|
this.maxOutputTokens = positiveInteger(options.maxOutputTokens ??
|
|
637
658
|
(options.model.includes('claude-opus-4-6')
|
|
@@ -640,7 +661,7 @@ export class AnthropicCompatibleProvider {
|
|
|
640
661
|
? 32_000
|
|
641
662
|
: 8192), 'Max output tokens');
|
|
642
663
|
this.capabilities = {
|
|
643
|
-
streaming:
|
|
664
|
+
streaming: this.streaming,
|
|
644
665
|
usage: true,
|
|
645
666
|
tools: true,
|
|
646
667
|
images: true,
|
|
@@ -712,7 +733,7 @@ export class AnthropicCompatibleProvider {
|
|
|
712
733
|
model: this.options.model,
|
|
713
734
|
max_tokens: maxTokens,
|
|
714
735
|
messages: serialized.messages,
|
|
715
|
-
stream:
|
|
736
|
+
stream: this.streaming,
|
|
716
737
|
...(thinkingPayload ? { thinking: thinkingPayload } : {}),
|
|
717
738
|
...(request.effort
|
|
718
739
|
? { output_config: { effort: request.effort } }
|
|
@@ -753,7 +774,9 @@ export class AnthropicCompatibleProvider {
|
|
|
753
774
|
requestInit.signal = request.signal;
|
|
754
775
|
let response;
|
|
755
776
|
try {
|
|
777
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
756
778
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
779
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
757
780
|
}
|
|
758
781
|
catch (error) {
|
|
759
782
|
const kind = transportFailureKind(error, request.signal);
|
|
@@ -784,6 +807,8 @@ export class AnthropicCompatibleProvider {
|
|
|
784
807
|
ended = true;
|
|
785
808
|
break;
|
|
786
809
|
}
|
|
810
|
+
if (value.byteLength > 0)
|
|
811
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
787
812
|
size += value.byteLength;
|
|
788
813
|
if (size > this.maxErrorBodyBytes) {
|
|
789
814
|
throw new ModelProviderError(`Provider error response exceeded ${this.maxErrorBodyBytes} bytes`, { retryable: false, status: response.status });
|
|
@@ -816,11 +841,144 @@ export class AnthropicCompatibleProvider {
|
|
|
816
841
|
status: response.status,
|
|
817
842
|
});
|
|
818
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
|
+
}
|
|
819
977
|
if (!response.body) {
|
|
820
|
-
throw new ModelProviderError('Provider response has no body', {
|
|
978
|
+
throw markNonStreamingFallbackEligible(new ModelProviderError('Provider response has no body', {
|
|
821
979
|
kind: 'transport_error',
|
|
822
980
|
retryable: true,
|
|
823
|
-
});
|
|
981
|
+
}));
|
|
824
982
|
}
|
|
825
983
|
const reader = response.body.getReader();
|
|
826
984
|
const decoder = new TextDecoder();
|
|
@@ -845,6 +1003,8 @@ export class AnthropicCompatibleProvider {
|
|
|
845
1003
|
try {
|
|
846
1004
|
stream: while (true) {
|
|
847
1005
|
const { done, value } = await reader.read();
|
|
1006
|
+
if (!done && value.byteLength > 0)
|
|
1007
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
848
1008
|
buffer += decoder.decode(value, { stream: !done });
|
|
849
1009
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
850
1010
|
if (Buffer.byteLength(buffer) > this.maxStreamBufferBytes) {
|
|
@@ -874,18 +1034,21 @@ export class AnthropicCompatibleProvider {
|
|
|
874
1034
|
}
|
|
875
1035
|
}
|
|
876
1036
|
if (!state.terminal) {
|
|
877
|
-
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 }));
|
|
878
1038
|
}
|
|
879
1039
|
}
|
|
880
1040
|
catch (error) {
|
|
881
1041
|
if (error instanceof ModelProviderError)
|
|
882
1042
|
throw error;
|
|
883
1043
|
const kind = transportFailureKind(error, request.signal);
|
|
884
|
-
|
|
1044
|
+
const streamError = new ModelProviderError('Provider stream failed', {
|
|
885
1045
|
kind,
|
|
886
1046
|
retryable: kind === 'timeout' || kind === 'transport_error',
|
|
887
1047
|
cause: error,
|
|
888
1048
|
});
|
|
1049
|
+
throw kind === 'timeout' || kind === 'transport_error'
|
|
1050
|
+
? markNonStreamingFallbackEligible(streamError)
|
|
1051
|
+
: streamError;
|
|
889
1052
|
}
|
|
890
1053
|
finally {
|
|
891
1054
|
if (!streamEnded) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ModelProviderError, } from '../core/runtime.js';
|
|
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;
|
|
@@ -572,7 +573,12 @@ function parseEvent(value, state, limits) {
|
|
|
572
573
|
parsed = JSON.parse(call.arguments || '{}');
|
|
573
574
|
}
|
|
574
575
|
catch {
|
|
575
|
-
|
|
576
|
+
if (!id || !name)
|
|
577
|
+
throw invalid('Codex provider returned an invalid tool call');
|
|
578
|
+
call.emitted = true;
|
|
579
|
+
state.toolCallEmitted = true;
|
|
580
|
+
events.push({ type: 'tool-call', call: malformedModelToolCall(id, name) });
|
|
581
|
+
return events;
|
|
576
582
|
}
|
|
577
583
|
if (!isRecord(parsed))
|
|
578
584
|
throw invalid('Codex provider returned non-object function arguments');
|
|
@@ -652,7 +658,7 @@ export function parseCodexSseFrame(data) {
|
|
|
652
658
|
reasoning: DEFAULT_MAX_REASONING,
|
|
653
659
|
});
|
|
654
660
|
}
|
|
655
|
-
async function drainBody(response, maxBytes) {
|
|
661
|
+
async function drainBody(response, maxBytes, request) {
|
|
656
662
|
const reader = response.body?.getReader();
|
|
657
663
|
if (!reader)
|
|
658
664
|
return;
|
|
@@ -665,6 +671,8 @@ async function drainBody(response, maxBytes) {
|
|
|
665
671
|
ended = true;
|
|
666
672
|
return;
|
|
667
673
|
}
|
|
674
|
+
if (next.value.byteLength > 0)
|
|
675
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
668
676
|
total += next.value.byteLength;
|
|
669
677
|
if (total > maxBytes)
|
|
670
678
|
return;
|
|
@@ -736,12 +744,14 @@ export class CodexSubscriptionProvider {
|
|
|
736
744
|
'content-type': 'application/json',
|
|
737
745
|
};
|
|
738
746
|
try {
|
|
747
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
739
748
|
response = await this.fetchImplementation(CODEX_RESPONSES_ENDPOINT, {
|
|
740
749
|
method: 'POST',
|
|
741
750
|
headers,
|
|
742
751
|
body: JSON.stringify(body),
|
|
743
752
|
...(signal ? { signal } : {}),
|
|
744
753
|
});
|
|
754
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
745
755
|
}
|
|
746
756
|
catch (error) {
|
|
747
757
|
throw transportError(error, signal);
|
|
@@ -749,12 +759,13 @@ export class CodexSubscriptionProvider {
|
|
|
749
759
|
if (response.status === 401 && !refreshed) {
|
|
750
760
|
refreshed = true;
|
|
751
761
|
try {
|
|
752
|
-
await drainBody(response, this.maxErrorBodyBytes);
|
|
762
|
+
await drainBody(response, this.maxErrorBodyBytes, request);
|
|
753
763
|
}
|
|
754
764
|
catch {
|
|
755
765
|
/* ignore unread body failures */
|
|
756
766
|
}
|
|
757
767
|
try {
|
|
768
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
758
769
|
access = await this.options.access({
|
|
759
770
|
forceAfter: access.accessToken,
|
|
760
771
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -767,7 +778,7 @@ export class CodexSubscriptionProvider {
|
|
|
767
778
|
}
|
|
768
779
|
if (!response.ok) {
|
|
769
780
|
try {
|
|
770
|
-
await drainBody(response, this.maxErrorBodyBytes);
|
|
781
|
+
await drainBody(response, this.maxErrorBodyBytes, request);
|
|
771
782
|
}
|
|
772
783
|
catch {
|
|
773
784
|
/* preserve generic error */
|
|
@@ -804,6 +815,8 @@ export class CodexSubscriptionProvider {
|
|
|
804
815
|
try {
|
|
805
816
|
while (true) {
|
|
806
817
|
const next = await reader.read();
|
|
818
|
+
if (!next.done && next.value.byteLength > 0)
|
|
819
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
807
820
|
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
808
821
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
809
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
|
-
import { ModelProviderError, } from '../core/runtime.js';
|
|
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
|
}
|
|
@@ -64,8 +65,16 @@ function completedToolCallEvents(pending) {
|
|
|
64
65
|
try {
|
|
65
66
|
input = JSON.parse(call.arguments || '{}');
|
|
66
67
|
}
|
|
67
|
-
catch
|
|
68
|
-
|
|
68
|
+
catch {
|
|
69
|
+
if (!call.id || !call.name) {
|
|
70
|
+
throw new ModelProviderError('Provider returned an invalid tool call', {
|
|
71
|
+
retryable: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: 'tool-call',
|
|
76
|
+
call: malformedModelToolCall(call.id, call.name),
|
|
77
|
+
};
|
|
69
78
|
}
|
|
70
79
|
if (!isRecord(input) || !call.id || !call.name) {
|
|
71
80
|
throw new ModelProviderError('Provider returned an invalid tool call', {
|
|
@@ -343,7 +352,9 @@ export class OpenAICompatibleProvider {
|
|
|
343
352
|
requestInit.signal = request.signal;
|
|
344
353
|
let response;
|
|
345
354
|
try {
|
|
355
|
+
reportProviderTransportActivity(request, 'request-started');
|
|
346
356
|
response = await this.fetchImplementation(this.endpoint, requestInit);
|
|
357
|
+
reportProviderTransportActivity(request, 'response-received');
|
|
347
358
|
}
|
|
348
359
|
catch (error) {
|
|
349
360
|
const kind = transportFailureKind(error, request.signal);
|
|
@@ -374,6 +385,8 @@ export class OpenAICompatibleProvider {
|
|
|
374
385
|
ended = true;
|
|
375
386
|
break;
|
|
376
387
|
}
|
|
388
|
+
if (value.byteLength > 0)
|
|
389
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
377
390
|
size += value.byteLength;
|
|
378
391
|
if (size > this.maxErrorBodyBytes) {
|
|
379
392
|
throw new ModelProviderError(`Provider error response exceeded ${this.maxErrorBodyBytes} bytes`, { retryable: false, status: response.status });
|
|
@@ -426,6 +439,8 @@ export class OpenAICompatibleProvider {
|
|
|
426
439
|
try {
|
|
427
440
|
stream: while (true) {
|
|
428
441
|
const { done, value } = await reader.read();
|
|
442
|
+
if (!done && value.byteLength > 0)
|
|
443
|
+
reportProviderTransportActivity(request, 'response-chunk');
|
|
429
444
|
buffer += decoder.decode(value, { stream: !done });
|
|
430
445
|
buffer = buffer.replaceAll('\r\n', '\n');
|
|
431
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
|