@inflexa-ai/harness 0.17.0 → 0.19.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/dist/agents/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/agents/report-session-agent.d.ts +56 -0
- package/dist/agents/report-session-agent.js +72 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +157 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +89 -0
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/providers/ai-sdk.d.ts +77 -0
- package/dist/providers/ai-sdk.js +149 -19
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/pin-snapshot.d.ts +30 -0
- package/dist/report-model/pin-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +72 -0
- package/dist/report-model/validate.js +95 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/assemble.d.ts +15 -0
- package/dist/runtime/assemble.js +28 -6
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +54 -0
- package/dist/state/purge-analysis.js +2 -0
- package/dist/state/report-session-state.d.ts +112 -0
- package/dist/state/report-session-state.js +150 -0
- package/dist/state/report-versions.d.ts +129 -0
- package/dist/state/report-versions.js +167 -0
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
- package/dist/tools/report-authoring/authoring-tools.js +460 -0
- package/dist/tools/report-session/index.d.ts +8 -0
- package/dist/tools/report-session/index.js +8 -0
- package/dist/tools/report-session/preview-report.d.ts +90 -0
- package/dist/tools/report-session/preview-report.js +245 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +7 -2
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -32,6 +32,16 @@ export interface AiSdkProviderDeps {
|
|
|
32
32
|
readonly logger?: Logger;
|
|
33
33
|
/** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
|
|
34
34
|
readonly maxOutputTokens?: number;
|
|
35
|
+
/** The retry limit of the harness envelope. Defaults to {@link RETRY_MAX_RETRIES}. */
|
|
36
|
+
readonly maxRetries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* The bound for each silent interval of a request attempt, in milliseconds.
|
|
39
|
+
* The provider hands the value to the SDK as the bound for each gap between
|
|
40
|
+
* two content chunks of a stream. The value also names itself in the error of
|
|
41
|
+
* a stream that the bound stopped. When the field is absent, the provider
|
|
42
|
+
* hands the SDK no bound.
|
|
43
|
+
*/
|
|
44
|
+
readonly requestTimeoutMs?: number;
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
47
|
* Connection + model configuration for one `ChatProvider`, discriminated by
|
|
@@ -52,6 +62,27 @@ export type AiSdkProviderConfig = {
|
|
|
52
62
|
readonly capabilities?: Partial<ProviderCapabilities>;
|
|
53
63
|
/** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
|
|
54
64
|
readonly maxOutputTokens?: number;
|
|
65
|
+
/**
|
|
66
|
+
* The bound for each silent interval of a request attempt, in
|
|
67
|
+
* milliseconds. The value bounds the wait until the response starts, and
|
|
68
|
+
* each gap between two content chunks of a stream, per attempt. A chunk
|
|
69
|
+
* that carries no content, for example a keep-alive comment, does not
|
|
70
|
+
* reset the gap bound. When the field is absent, the provider installs no
|
|
71
|
+
* guard, and its behavior does not change.
|
|
72
|
+
*
|
|
73
|
+
* The harness lifts the transport floor of Bun: the guard adds
|
|
74
|
+
* `timeout: false` to the fetch init, thus the idle cut of 300 seconds
|
|
75
|
+
* does not apply. Under Node the floor of undici stays. Thus a Node
|
|
76
|
+
* embedder that sets a value above 300 seconds supplies a `fetch` whose
|
|
77
|
+
* dispatcher raises that limit.
|
|
78
|
+
*/
|
|
79
|
+
readonly requestTimeoutMs?: number;
|
|
80
|
+
/**
|
|
81
|
+
* The retry limit of the harness envelope. The envelope uses this value
|
|
82
|
+
* in place of the default limit. When the field is absent, the envelope
|
|
83
|
+
* keeps the default of 10.
|
|
84
|
+
*/
|
|
85
|
+
readonly maxRetries?: number;
|
|
55
86
|
} | {
|
|
56
87
|
readonly kind: "openai-compatible";
|
|
57
88
|
readonly name: string;
|
|
@@ -62,6 +93,27 @@ export type AiSdkProviderConfig = {
|
|
|
62
93
|
readonly capabilities?: Partial<ProviderCapabilities>;
|
|
63
94
|
/** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
|
|
64
95
|
readonly maxOutputTokens?: number;
|
|
96
|
+
/**
|
|
97
|
+
* The bound for each silent interval of a request attempt, in
|
|
98
|
+
* milliseconds. The value bounds the wait until the response starts, and
|
|
99
|
+
* each gap between two content chunks of a stream, per attempt. A chunk
|
|
100
|
+
* that carries no content, for example a keep-alive comment, does not
|
|
101
|
+
* reset the gap bound. When the field is absent, the provider installs no
|
|
102
|
+
* guard, and its behavior does not change.
|
|
103
|
+
*
|
|
104
|
+
* The harness lifts the transport floor of Bun: the guard adds
|
|
105
|
+
* `timeout: false` to the fetch init, thus the idle cut of 300 seconds
|
|
106
|
+
* does not apply. Under Node the floor of undici stays. Thus a Node
|
|
107
|
+
* embedder that sets a value above 300 seconds supplies a `fetch` whose
|
|
108
|
+
* dispatcher raises that limit.
|
|
109
|
+
*/
|
|
110
|
+
readonly requestTimeoutMs?: number;
|
|
111
|
+
/**
|
|
112
|
+
* The retry limit of the harness envelope. The envelope uses this value
|
|
113
|
+
* in place of the default limit. When the field is absent, the envelope
|
|
114
|
+
* keeps the default of 10.
|
|
115
|
+
*/
|
|
116
|
+
readonly maxRetries?: number;
|
|
65
117
|
};
|
|
66
118
|
export interface ConfiguredAiSdkProviderDeps {
|
|
67
119
|
readonly config: AiSdkProviderConfig;
|
|
@@ -78,11 +130,36 @@ export interface ConfiguredAiSdkProviderDeps {
|
|
|
78
130
|
*/
|
|
79
131
|
export declare function computeRetryDelayMs(error: unknown, exponentialBackoffDelay: number, random?: () => number): number;
|
|
80
132
|
export declare function createAiSdkProvider(deps: AiSdkProviderDeps): ChatProvider;
|
|
133
|
+
/**
|
|
134
|
+
* Wrap a fetch so that the wait until the response starts does not exceed
|
|
135
|
+
* `requestTimeoutMs`. The wrapper arms one timer for each fetch call. The timer
|
|
136
|
+
* runs from the send until the fetch promise settles, and the headers of the
|
|
137
|
+
* response settle it. On expiry the wrapper aborts a dedicated controller with a
|
|
138
|
+
* `RequestTimeoutError` reason, and it composes that controller with the caller
|
|
139
|
+
* signal through `AbortSignal.any`. Thus the caller signal keeps its normal
|
|
140
|
+
* effect, and a caller abort stays a cancellation.
|
|
141
|
+
*
|
|
142
|
+
* The guard bounds the response start only. The SDK bounds each later gap of a
|
|
143
|
+
* streamed body with its own `chunkMs` setting, thus no timer of the wrapper
|
|
144
|
+
* survives the headers. Thus a stream with steady content does not trip the
|
|
145
|
+
* guard, whatever its total length. One fetch call is one attempt, because the
|
|
146
|
+
* SDK retries are off, thus the window is per attempt.
|
|
147
|
+
*
|
|
148
|
+
* When the field is absent, `createConfiguredAiSdkProvider` installs no wrapper,
|
|
149
|
+
* and the behavior stays byte-identical.
|
|
150
|
+
*/
|
|
151
|
+
export declare function wrapFetchWithRequestTimeout(fetchImpl: FetchLike, requestTimeoutMs: number): FetchLike;
|
|
81
152
|
/**
|
|
82
153
|
* Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
|
|
83
154
|
* connection and model. The model is closed into the returned provider here (it
|
|
84
155
|
* is never passed per `ChatRequest`), so N models require N provider
|
|
85
156
|
* instances over one shared connection configuration — one instance serves the
|
|
86
157
|
* single model it was built with.
|
|
158
|
+
*
|
|
159
|
+
* When the config sets `requestTimeoutMs`, the provider wraps the effective fetch
|
|
160
|
+
* (`config.fetch ?? globalThis.fetch`) with the response-start guard, it hands
|
|
161
|
+
* the value to the stream call as the SDK chunk bound, and it advertises the
|
|
162
|
+
* value on the returned instance. When the field is absent, it installs no
|
|
163
|
+
* wrapper, it hands the SDK no bound, and it advertises no value.
|
|
87
164
|
*/
|
|
88
165
|
export declare function createConfiguredAiSdkProvider(deps: ConfiguredAiSdkProviderDeps): ChatProvider;
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateText, streamText, wrapLanguageModel } from "ai";
|
|
1
|
+
import { generateText, streamText, wrapLanguageModel, } from "ai";
|
|
2
2
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
3
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
4
4
|
import { APICallError } from "@ai-sdk/provider";
|
|
@@ -6,7 +6,7 @@ import { retryWithExponentialBackoff } from "@ai-sdk/provider-utils";
|
|
|
6
6
|
import { ResultAsync, err, ok } from "neverthrow";
|
|
7
7
|
import { scopeWorkloadId } from "../auth/types.js";
|
|
8
8
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
9
|
-
import { classifyProviderError, toProviderError } from "./errors.js";
|
|
9
|
+
import { classifyProviderError, RequestTimeoutError, toProviderError } from "./errors.js";
|
|
10
10
|
/**
|
|
11
11
|
* The harness-owned retry envelope. The AI SDK's built-in retry exposes only a
|
|
12
12
|
* count: its 2s initial delay and ×2 factor are fixed, it caps neither the
|
|
@@ -189,15 +189,20 @@ function unwrapForClassification(e) {
|
|
|
189
189
|
* that, once retries are exhausted, `toProviderError`'s status walk reaches the
|
|
190
190
|
* true HTTP status instead of stopping at a synthetic wrapper. The attempt
|
|
191
191
|
* counter is per-instance because each call builds its own retry.
|
|
192
|
+
*
|
|
193
|
+
* An aborted caller signal also stops the retry. A wall-clock guard of a caller
|
|
194
|
+
* aborts with a `TimeoutError` reason, which the taxonomy marks as a retryable
|
|
195
|
+
* timeout. Without this guard the envelope would loop an expiry that the caller
|
|
196
|
+
* itself declared.
|
|
192
197
|
*/
|
|
193
|
-
function createRetry(signal, logger) {
|
|
198
|
+
function createRetry(signal, logger, maxRetries) {
|
|
194
199
|
let retryCount = 0;
|
|
195
200
|
return retryWithExponentialBackoff({
|
|
196
|
-
maxRetries
|
|
201
|
+
maxRetries,
|
|
197
202
|
initialDelayInMs: RETRY_INITIAL_DELAY_MS,
|
|
198
203
|
backoffFactor: RETRY_BACKOFF_FACTOR,
|
|
199
204
|
abortSignal: signal,
|
|
200
|
-
shouldRetry: (e) => !(e instanceof BillingSeamFailure) && classifyProviderError(e).retryable,
|
|
205
|
+
shouldRetry: (e) => !(e instanceof BillingSeamFailure) && signal?.aborted !== true && classifyProviderError(e).retryable,
|
|
201
206
|
getDelayInMs: ({ error, exponentialBackoffDelay }) => {
|
|
202
207
|
const delayMs = computeRetryDelayMs(error, exponentialBackoffDelay);
|
|
203
208
|
retryCount += 1;
|
|
@@ -329,15 +334,52 @@ function sanitizeMessages(messages) {
|
|
|
329
334
|
}
|
|
330
335
|
return out;
|
|
331
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Advance the SDK stream to the next text delta.
|
|
339
|
+
*
|
|
340
|
+
* The full stream carries one part for each event of a turn, and only a text
|
|
341
|
+
* part reaches the consumer of the harness. Thus the pull skips each other part.
|
|
342
|
+
* The `abort` part stops the pull, because the SDK closes the stream directly
|
|
343
|
+
* after it. Thus a timeout can never end the stream in silence with the text
|
|
344
|
+
* that arrived before it.
|
|
345
|
+
*/
|
|
346
|
+
async function pullNextDelta(iterator) {
|
|
347
|
+
for (let next = await iterator.next(); next.done !== true; next = await iterator.next()) {
|
|
348
|
+
const part = next.value;
|
|
349
|
+
if (part.type === "text-delta")
|
|
350
|
+
return { kind: "delta", text: part.text };
|
|
351
|
+
if (part.type === "abort")
|
|
352
|
+
return { kind: "aborted", reason: part.reason };
|
|
353
|
+
}
|
|
354
|
+
return { kind: "end" };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The failure that an `abort` part of the SDK stream stands for.
|
|
358
|
+
*
|
|
359
|
+
* The part carries a message only, thus the caller signal is the one thing that
|
|
360
|
+
* separates a cancellation from a timeout. When the caller signal is aborted,
|
|
361
|
+
* the reason of that signal rides out, and the cancellation path applies
|
|
362
|
+
* unchanged. Otherwise the SDK chunk bound stopped the stream, and the sentinel
|
|
363
|
+
* names the configured value for the classifier.
|
|
364
|
+
*/
|
|
365
|
+
function abortedStreamFailure(signal, reason, requestTimeoutMs) {
|
|
366
|
+
if (signal?.aborted === true)
|
|
367
|
+
return signal.reason;
|
|
368
|
+
if (requestTimeoutMs !== undefined)
|
|
369
|
+
return new RequestTimeoutError(requestTimeoutMs);
|
|
370
|
+
return new Error(reason ?? "The provider aborted the stream.");
|
|
371
|
+
}
|
|
332
372
|
export function createAiSdkProvider(deps) {
|
|
333
373
|
const capabilities = { toolCalling: deps.capabilities?.toolCalling ?? true };
|
|
334
374
|
const logger = (deps.logger ?? createNoopLogger()).named("providers.ai-sdk");
|
|
335
375
|
const maxOutputTokens = deps.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
376
|
+
const maxRetries = deps.maxRetries ?? RETRY_MAX_RETRIES;
|
|
377
|
+
const requestTimeoutMs = deps.requestTimeoutMs;
|
|
336
378
|
const requestedModelId = requestedModelIdOf(deps.model);
|
|
337
379
|
routeSdkWarningsTo(logger);
|
|
338
380
|
function chat(req, session, signal) {
|
|
339
381
|
const run = async () => {
|
|
340
|
-
const retry = createRetry(signal, logger);
|
|
382
|
+
const retry = createRetry(signal, logger, maxRetries);
|
|
341
383
|
const capture = captureServedModelId(deps.model);
|
|
342
384
|
try {
|
|
343
385
|
const result = await retry(async () => {
|
|
@@ -357,6 +399,10 @@ export function createAiSdkProvider(deps) {
|
|
|
357
399
|
maxRetries: 0,
|
|
358
400
|
headers,
|
|
359
401
|
abortSignal: signal,
|
|
402
|
+
// No SDK timeout rides here. `chunkMs` bounds a stream only,
|
|
403
|
+
// thus it is inert on this call. The fetch guard is the full
|
|
404
|
+
// bound, because the headers of a non-streaming response arrive
|
|
405
|
+
// when the model completes.
|
|
360
406
|
providerOptions: req.providerOptions,
|
|
361
407
|
});
|
|
362
408
|
});
|
|
@@ -371,7 +417,7 @@ export function createAiSdkProvider(deps) {
|
|
|
371
417
|
return new ResultAsync(run());
|
|
372
418
|
}
|
|
373
419
|
async function* chatStream(req, session, signal) {
|
|
374
|
-
const retry = createRetry(signal, logger);
|
|
420
|
+
const retry = createRetry(signal, logger, maxRetries);
|
|
375
421
|
const capture = captureServedModelId(deps.model);
|
|
376
422
|
try {
|
|
377
423
|
// Retry covers only stream establishment: streamText defers wire errors
|
|
@@ -395,11 +441,22 @@ export function createAiSdkProvider(deps) {
|
|
|
395
441
|
maxRetries: 0,
|
|
396
442
|
headers,
|
|
397
443
|
abortSignal: signal,
|
|
444
|
+
// The SDK bounds each gap between two content chunks of the body.
|
|
445
|
+
// Its timer arms at the first part of the provider stream, thus the
|
|
446
|
+
// wait until the response starts stays with the fetch guard. A
|
|
447
|
+
// chunk that carries no content, for example a keep-alive comment,
|
|
448
|
+
// produces no part, thus it does not reset this bound.
|
|
449
|
+
...(requestTimeoutMs !== undefined ? { timeout: { chunkMs: requestTimeoutMs } } : {}),
|
|
398
450
|
providerOptions: req.providerOptions,
|
|
399
451
|
});
|
|
400
|
-
const iterator = result.
|
|
401
|
-
const first = await iterator
|
|
402
|
-
|
|
452
|
+
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
453
|
+
const first = await pullNextDelta(iterator);
|
|
454
|
+
// The chunk bound can stop the stream before any content arrives. The
|
|
455
|
+
// throw carries the sentinel, thus the envelope gives the attempt a
|
|
456
|
+
// fresh window under the retry policy of a connection error.
|
|
457
|
+
if (first.kind === "aborted")
|
|
458
|
+
throw abortedStreamFailure(signal, first.reason, requestTimeoutMs);
|
|
459
|
+
if (first.kind === "end") {
|
|
403
460
|
// A doStream promise rejection — the shape a real connection
|
|
404
461
|
// failure takes — does NOT surface at this first pull: streamText
|
|
405
462
|
// resolves it `{ done: true }` and defers the rejection to the
|
|
@@ -408,8 +465,8 @@ export function createAiSdkProvider(deps) {
|
|
|
408
465
|
// retried; a genuine text-less turn resolves them, and the terminal
|
|
409
466
|
// event is built from these already-resolved values without a
|
|
410
467
|
// second await. (These promises only settle once the stream is
|
|
411
|
-
// fully drained, so they can be awaited here only because the
|
|
412
|
-
//
|
|
468
|
+
// fully drained, so they can be awaited here only because the pull
|
|
469
|
+
// already reported the stream done.)
|
|
413
470
|
return {
|
|
414
471
|
kind: "completed",
|
|
415
472
|
messages: await result.responseMessages,
|
|
@@ -418,7 +475,7 @@ export function createAiSdkProvider(deps) {
|
|
|
418
475
|
usage: await result.usage,
|
|
419
476
|
};
|
|
420
477
|
}
|
|
421
|
-
return { kind: "streaming", result, iterator, firstDelta: first.
|
|
478
|
+
return { kind: "streaming", result, iterator, firstDelta: first.text };
|
|
422
479
|
});
|
|
423
480
|
if (opened.kind === "completed") {
|
|
424
481
|
const response = responseFromMessages({
|
|
@@ -438,9 +495,14 @@ export function createAiSdkProvider(deps) {
|
|
|
438
495
|
yield { type: "text-delta", text: firstDelta };
|
|
439
496
|
// Drain the same iterator: past the first delta, errors propagate
|
|
440
497
|
// unchanged (no retry, no re-emit).
|
|
441
|
-
for (let
|
|
442
|
-
|
|
443
|
-
|
|
498
|
+
for (let pull = await pullNextDelta(iterator); pull.kind !== "end"; pull = await pullNextDelta(iterator)) {
|
|
499
|
+
// A mid-stream abort ends the turn with an error, never with the
|
|
500
|
+
// partial text that arrived before it. The catch below builds the
|
|
501
|
+
// terminal `ProviderError` from the throwable.
|
|
502
|
+
if (pull.kind === "aborted")
|
|
503
|
+
throw abortedStreamFailure(signal, pull.reason, requestTimeoutMs);
|
|
504
|
+
text += pull.text;
|
|
505
|
+
yield { type: "text-delta", text: pull.text };
|
|
444
506
|
}
|
|
445
507
|
const response = responseFromMessages({
|
|
446
508
|
messages: await result.responseMessages,
|
|
@@ -461,7 +523,63 @@ export function createAiSdkProvider(deps) {
|
|
|
461
523
|
throw toProviderError(unwrapForClassification(e), workloadOf(session));
|
|
462
524
|
}
|
|
463
525
|
}
|
|
464
|
-
|
|
526
|
+
// The provider advertises the guard limit on its own instance, thus a
|
|
527
|
+
// consumer that scales a deadline reads it from the provider in its deps.
|
|
528
|
+
return { capabilities, chat, chatStream, ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}) };
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Wrap a fetch so that the wait until the response starts does not exceed
|
|
532
|
+
* `requestTimeoutMs`. The wrapper arms one timer for each fetch call. The timer
|
|
533
|
+
* runs from the send until the fetch promise settles, and the headers of the
|
|
534
|
+
* response settle it. On expiry the wrapper aborts a dedicated controller with a
|
|
535
|
+
* `RequestTimeoutError` reason, and it composes that controller with the caller
|
|
536
|
+
* signal through `AbortSignal.any`. Thus the caller signal keeps its normal
|
|
537
|
+
* effect, and a caller abort stays a cancellation.
|
|
538
|
+
*
|
|
539
|
+
* The guard bounds the response start only. The SDK bounds each later gap of a
|
|
540
|
+
* streamed body with its own `chunkMs` setting, thus no timer of the wrapper
|
|
541
|
+
* survives the headers. Thus a stream with steady content does not trip the
|
|
542
|
+
* guard, whatever its total length. One fetch call is one attempt, because the
|
|
543
|
+
* SDK retries are off, thus the window is per attempt.
|
|
544
|
+
*
|
|
545
|
+
* When the field is absent, `createConfiguredAiSdkProvider` installs no wrapper,
|
|
546
|
+
* and the behavior stays byte-identical.
|
|
547
|
+
*/
|
|
548
|
+
export function wrapFetchWithRequestTimeout(fetchImpl, requestTimeoutMs) {
|
|
549
|
+
return async (input, init) => {
|
|
550
|
+
const controller = new AbortController();
|
|
551
|
+
const callerSignal = init?.signal ?? undefined;
|
|
552
|
+
const timer = setTimeout(() => controller.abort(new RequestTimeoutError(requestTimeoutMs)), requestTimeoutMs);
|
|
553
|
+
// The harness runs on Node, where a timer has `unref`. Unref the guard
|
|
554
|
+
// timer, thus an armed guard does not keep a short-lived process alive.
|
|
555
|
+
// The DOM type of `setTimeout` declares no `unref`, thus guard the call.
|
|
556
|
+
timer.unref?.();
|
|
557
|
+
const clear = () => {
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
// The harness runs on Node, where a caller signal can outlive one fetch
|
|
560
|
+
// attempt. Remove the abort listener when the attempt settles, thus a
|
|
561
|
+
// long-lived signal does not retain this closure.
|
|
562
|
+
callerSignal?.removeEventListener("abort", clear);
|
|
563
|
+
};
|
|
564
|
+
// Compose the guard controller with the caller signal, thus either one
|
|
565
|
+
// can abort the wire, and the abort reason names its own source. A caller
|
|
566
|
+
// abort also clears the guard timer at once. Thus an armed timer does not
|
|
567
|
+
// reach a pointless expiry after the caller cancels.
|
|
568
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
|
|
569
|
+
callerSignal?.addEventListener("abort", clear, { once: true });
|
|
570
|
+
// `timeout: false` is an extension of the fetch init of Bun. Bun cuts a
|
|
571
|
+
// request that stays idle for 300 seconds, and this key lifts that cut. Node
|
|
572
|
+
// ignores an unknown member of the init, thus the key is inert there. A
|
|
573
|
+
// probe on Bun 1.3.14 proved both results. The `RequestInit` type declares
|
|
574
|
+
// no such member, thus the cast.
|
|
575
|
+
const forwarded = { ...init, signal, timeout: false };
|
|
576
|
+
try {
|
|
577
|
+
return await fetchImpl(input, forwarded);
|
|
578
|
+
}
|
|
579
|
+
finally {
|
|
580
|
+
clear();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
465
583
|
}
|
|
466
584
|
/**
|
|
467
585
|
* Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
|
|
@@ -469,14 +587,22 @@ export function createAiSdkProvider(deps) {
|
|
|
469
587
|
* is never passed per `ChatRequest`), so N models require N provider
|
|
470
588
|
* instances over one shared connection configuration — one instance serves the
|
|
471
589
|
* single model it was built with.
|
|
590
|
+
*
|
|
591
|
+
* When the config sets `requestTimeoutMs`, the provider wraps the effective fetch
|
|
592
|
+
* (`config.fetch ?? globalThis.fetch`) with the response-start guard, it hands
|
|
593
|
+
* the value to the stream call as the SDK chunk bound, and it advertises the
|
|
594
|
+
* value on the returned instance. When the field is absent, it installs no
|
|
595
|
+
* wrapper, it hands the SDK no bound, and it advertises no value.
|
|
472
596
|
*/
|
|
473
597
|
export function createConfiguredAiSdkProvider(deps) {
|
|
474
598
|
const config = deps.config;
|
|
599
|
+
const requestTimeoutMs = config.requestTimeoutMs;
|
|
600
|
+
const effectiveFetch = requestTimeoutMs === undefined ? config.fetch : wrapFetchWithRequestTimeout(config.fetch ?? globalThis.fetch, requestTimeoutMs);
|
|
475
601
|
if (config.kind === "anthropic") {
|
|
476
602
|
const provider = createAnthropic({
|
|
477
603
|
baseURL: config.baseURL,
|
|
478
604
|
apiKey: config.apiKey,
|
|
479
|
-
fetch:
|
|
605
|
+
fetch: effectiveFetch,
|
|
480
606
|
});
|
|
481
607
|
return createAiSdkProvider({
|
|
482
608
|
model: provider.chat(config.model),
|
|
@@ -484,13 +610,15 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
484
610
|
capabilities: config.capabilities,
|
|
485
611
|
logger: deps.logger,
|
|
486
612
|
...(config.maxOutputTokens !== undefined ? { maxOutputTokens: config.maxOutputTokens } : {}),
|
|
613
|
+
...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
|
|
614
|
+
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
|
|
487
615
|
});
|
|
488
616
|
}
|
|
489
617
|
const provider = createOpenAICompatible({
|
|
490
618
|
name: config.name,
|
|
491
619
|
baseURL: config.baseURL,
|
|
492
620
|
apiKey: config.apiKey,
|
|
493
|
-
fetch:
|
|
621
|
+
fetch: effectiveFetch,
|
|
494
622
|
});
|
|
495
623
|
return createAiSdkProvider({
|
|
496
624
|
model: provider.chatModel(config.model),
|
|
@@ -498,5 +626,7 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
498
626
|
capabilities: config.capabilities,
|
|
499
627
|
logger: deps.logger,
|
|
500
628
|
...(config.maxOutputTokens !== undefined ? { maxOutputTokens: config.maxOutputTokens } : {}),
|
|
629
|
+
...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
|
|
630
|
+
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
|
|
501
631
|
});
|
|
502
632
|
}
|
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
* re-authenticating fixes it — which is a different remedy from every other
|
|
16
16
|
* non-retryable 4xx, so it is a different kind.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* The sentinel that the request-timeout guard uses as an abort reason. The guard
|
|
20
|
+
* arms a timer for each fetch attempt. On expiry the guard aborts the attempt
|
|
21
|
+
* with this value. The value rides the abort chain, thus the classifier finds it
|
|
22
|
+
* and marks the attempt as a retryable provider timeout.
|
|
23
|
+
*
|
|
24
|
+
* The name is not `AbortError`, thus the abort-detection paths do not swallow a
|
|
25
|
+
* guard abort as a caller cancellation. The message names the configured value.
|
|
26
|
+
*/
|
|
27
|
+
export declare class RequestTimeoutError extends Error {
|
|
28
|
+
/** The configured bound for one silent interval, in milliseconds. */
|
|
29
|
+
readonly requestTimeoutMs: number;
|
|
30
|
+
constructor(requestTimeoutMs: number);
|
|
31
|
+
}
|
|
18
32
|
export type ProviderErrorKind = "auth" | "budget" | "tenant-blocked" | "provider";
|
|
19
33
|
/**
|
|
20
34
|
* The provider error value channel — a `DomainError`-conforming
|
package/dist/providers/errors.js
CHANGED
|
@@ -16,6 +16,24 @@
|
|
|
16
16
|
* non-retryable 4xx, so it is a different kind.
|
|
17
17
|
*/
|
|
18
18
|
import { redactSecrets } from "../input-sanitization.js";
|
|
19
|
+
/**
|
|
20
|
+
* The sentinel that the request-timeout guard uses as an abort reason. The guard
|
|
21
|
+
* arms a timer for each fetch attempt. On expiry the guard aborts the attempt
|
|
22
|
+
* with this value. The value rides the abort chain, thus the classifier finds it
|
|
23
|
+
* and marks the attempt as a retryable provider timeout.
|
|
24
|
+
*
|
|
25
|
+
* The name is not `AbortError`, thus the abort-detection paths do not swallow a
|
|
26
|
+
* guard abort as a caller cancellation. The message names the configured value.
|
|
27
|
+
*/
|
|
28
|
+
export class RequestTimeoutError extends Error {
|
|
29
|
+
/** The configured bound for one silent interval, in milliseconds. */
|
|
30
|
+
requestTimeoutMs;
|
|
31
|
+
constructor(requestTimeoutMs) {
|
|
32
|
+
super(`The provider sent no data within the request timeout of ${requestTimeoutMs} ms.`);
|
|
33
|
+
this.name = "RequestTimeoutError";
|
|
34
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* Structural guard for an already-constructed `ProviderError` value. A
|
|
21
39
|
* `ProviderError` is a plain object, not an `Error`, so it cannot be
|
|
@@ -46,6 +64,20 @@ export function isProviderError(value) {
|
|
|
46
64
|
export function toProviderError(e, workload) {
|
|
47
65
|
if (isProviderError(e))
|
|
48
66
|
return e;
|
|
67
|
+
// A guard abort names the configured value in its message. Compose a message
|
|
68
|
+
// that also names the workload, thus a reader sees both. This runs before the
|
|
69
|
+
// status walk, because a guard abort carries no HTTP status. A timeout of the
|
|
70
|
+
// SDK carries no sentinel, thus it falls to the composed arm below, which
|
|
71
|
+
// forwards the message of the SDK.
|
|
72
|
+
const timeout = findRequestTimeout(e);
|
|
73
|
+
if (timeout !== undefined) {
|
|
74
|
+
return {
|
|
75
|
+
type: "provider",
|
|
76
|
+
retryable: true,
|
|
77
|
+
message: `The provider sent no data for ${workload} within the request timeout of ${timeout.requestTimeoutMs} ms.`,
|
|
78
|
+
cause: e,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
49
81
|
const { kind, retryable } = classifyProviderError(e);
|
|
50
82
|
const detail = e instanceof Error ? e.message : String(e);
|
|
51
83
|
if (kind === "auth") {
|
|
@@ -149,6 +181,22 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
149
181
|
"UND_ERR_SOCKET",
|
|
150
182
|
]);
|
|
151
183
|
const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|connection (?:refused|reset|error|closed)|econnrefused|enotfound|etimedout|terminated/i;
|
|
184
|
+
/**
|
|
185
|
+
* Walk the `cause` chain up to {@link MAX_CAUSE_HOPS} links, and return the first
|
|
186
|
+
* result that `probe` gives. `probe` gives a value for a link that matches, or
|
|
187
|
+
* `undefined` for a link that does not. The walk stops at the first match, at
|
|
188
|
+
* the hop cap, or at the end of the chain.
|
|
189
|
+
*/
|
|
190
|
+
function findInCauseChain(err, probe) {
|
|
191
|
+
let cursor = err;
|
|
192
|
+
for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
|
|
193
|
+
const found = probe(cursor);
|
|
194
|
+
if (found !== undefined)
|
|
195
|
+
return found;
|
|
196
|
+
cursor = cursor.cause;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
152
200
|
/**
|
|
153
201
|
* Walk the `cause` chain for the first numeric HTTP status. The single status
|
|
154
202
|
* walker: `classifyProviderError` keys on what this returns, so a caller asking
|
|
@@ -156,16 +204,14 @@ const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|conn
|
|
|
156
204
|
* than re-implement the traversal.
|
|
157
205
|
*/
|
|
158
206
|
export function extractStatus(err) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const e = cursor;
|
|
207
|
+
return findInCauseChain(err, (link) => {
|
|
208
|
+
const e = link;
|
|
162
209
|
if (typeof e.status === "number")
|
|
163
210
|
return e.status;
|
|
164
211
|
if (typeof e.statusCode === "number")
|
|
165
212
|
return e.statusCode;
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
213
|
+
return undefined;
|
|
214
|
+
});
|
|
169
215
|
}
|
|
170
216
|
/**
|
|
171
217
|
* Walk the `cause` chain for the first captured provider response body.
|
|
@@ -186,23 +232,54 @@ function extractResponseBody(err) {
|
|
|
186
232
|
}
|
|
187
233
|
return undefined;
|
|
188
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Walk the `cause` chain for the request-timeout sentinel. The guard aborts a
|
|
237
|
+
* fetch with a `RequestTimeoutError`. The SDK carries the abort reason down the
|
|
238
|
+
* `cause` chain, thus the sentinel can sit one or more hops down.
|
|
239
|
+
*/
|
|
240
|
+
function findRequestTimeout(err) {
|
|
241
|
+
return findInCauseChain(err, (link) => {
|
|
242
|
+
if (link instanceof RequestTimeoutError)
|
|
243
|
+
return link;
|
|
244
|
+
// A duplicate class identity across a realm boundary fails `instanceof`,
|
|
245
|
+
// thus match the name and the field as a fallback.
|
|
246
|
+
const e = link;
|
|
247
|
+
if (e.name === "RequestTimeoutError" && typeof e.requestTimeoutMs === "number")
|
|
248
|
+
return link;
|
|
249
|
+
return undefined;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The name of the abort reason that the `timeout` setting of the SDK raises. The
|
|
254
|
+
* SDK bounds each gap between two content chunks of a stream. On expiry it
|
|
255
|
+
* aborts the call with a bare `DOMException` that has this name, no cause, and
|
|
256
|
+
* no HTTP status. The message of that `DOMException` names the configured value.
|
|
257
|
+
*/
|
|
258
|
+
const SDK_TIMEOUT_ERROR_NAME = "TimeoutError";
|
|
259
|
+
/**
|
|
260
|
+
* Walk the `cause` chain for the timeout that the SDK raises. The `DOMException`
|
|
261
|
+
* carries no field of its own, thus the name is the one signal. The SDK carries
|
|
262
|
+
* an abort reason down the `cause` chain, thus the reason can sit one or more
|
|
263
|
+
* hops down.
|
|
264
|
+
*/
|
|
265
|
+
function findSdkTimeout(err) {
|
|
266
|
+
return findInCauseChain(err, (link) => {
|
|
267
|
+
const e = link;
|
|
268
|
+
return e.name === SDK_TIMEOUT_ERROR_NAME ? link : undefined;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
189
271
|
/** Does any link of the `cause` chain look like a transport-level failure? */
|
|
190
272
|
function looksLikeConnectionError(err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (typeof e.code === "string" && CONNECTION_ERROR_CODES.has(e.code)) {
|
|
273
|
+
return (findInCauseChain(err, (link) => {
|
|
274
|
+
const e = link;
|
|
275
|
+
if (typeof e.code === "string" && CONNECTION_ERROR_CODES.has(e.code))
|
|
195
276
|
return true;
|
|
196
|
-
|
|
197
|
-
if (typeof e.name === "string" && /^APIConnection/.test(e.name)) {
|
|
277
|
+
if (typeof e.name === "string" && /^APIConnection/.test(e.name))
|
|
198
278
|
return true;
|
|
199
|
-
|
|
200
|
-
if (typeof e.message === "string" && CONNECTION_ERROR_PATTERN.test(e.message)) {
|
|
279
|
+
if (typeof e.message === "string" && CONNECTION_ERROR_PATTERN.test(e.message))
|
|
201
280
|
return true;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
return false;
|
|
281
|
+
return undefined;
|
|
282
|
+
}) ?? false);
|
|
206
283
|
}
|
|
207
284
|
/**
|
|
208
285
|
* Classify a provider failure by origin.
|
|
@@ -214,6 +291,14 @@ function looksLikeConnectionError(err) {
|
|
|
214
291
|
* - Other `4xx` and parse / unknown errors → `provider`, not retryable.
|
|
215
292
|
*/
|
|
216
293
|
export function classifyProviderError(e) {
|
|
294
|
+
// The request-timeout guard aborts an attempt with a typed sentinel, and the
|
|
295
|
+
// chunk bound of the SDK aborts one with a `TimeoutError`. Each classifies as
|
|
296
|
+
// a retryable provider timeout, thus the envelope retries it under the same
|
|
297
|
+
// policy as a connection error. This runs before the status and the
|
|
298
|
+
// connection paths, because a timeout carries no HTTP status, and it must not
|
|
299
|
+
// fall to a non-retryable path.
|
|
300
|
+
if (findRequestTimeout(e) !== undefined || findSdkTimeout(e) !== undefined)
|
|
301
|
+
return { kind: "provider", retryable: true };
|
|
217
302
|
const status = extractStatus(e);
|
|
218
303
|
// `auth` is its own kind rather than a plain non-retryable 4xx because the
|
|
219
304
|
// remedy is categorically different: the request was well-formed and the
|
|
@@ -109,7 +109,23 @@ export interface AgentChat {
|
|
|
109
109
|
}
|
|
110
110
|
export interface ChatProvider extends AgentChat {
|
|
111
111
|
chatStream(req: ChatRequest, session: AgentSession, signal?: AbortSignal): AsyncIterable<ChatStreamEvent>;
|
|
112
|
+
/**
|
|
113
|
+
* The request-timeout limit that this provider enforces, in milliseconds, or
|
|
114
|
+
* absent when it enforces none. The value bounds each silent interval of a
|
|
115
|
+
* request attempt: the wait until the response starts, and each gap between
|
|
116
|
+
* two content chunks of a stream. A consumer that scales a deadline reads this
|
|
117
|
+
* value from the provider instance, not from a harness constant.
|
|
118
|
+
*/
|
|
119
|
+
readonly requestTimeoutMs?: number;
|
|
112
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The effective deadline in milliseconds for a provider-bound operation.
|
|
123
|
+
*
|
|
124
|
+
* An explicit value wins. Absent an explicit value, the deadline is the maximum
|
|
125
|
+
* of `floorMs` and the request-timeout limit that the provider advertises. Thus
|
|
126
|
+
* a configured request timeout can raise the floor. It cannot lower the floor.
|
|
127
|
+
*/
|
|
128
|
+
export declare function effectiveDeadlineMs(provider: Pick<ChatProvider, "requestTimeoutMs">, floorMs: number, explicitMs?: number): number;
|
|
113
129
|
export interface EmbeddingProvider {
|
|
114
130
|
/**
|
|
115
131
|
* Width of every vector `embed` returns. The write-side index paths create
|
package/dist/providers/types.js
CHANGED
|
@@ -6,4 +6,13 @@
|
|
|
6
6
|
* attribution is resolved lazily at the provider wire boundary.
|
|
7
7
|
*/
|
|
8
8
|
const _assertChatUsageFields = true;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The effective deadline in milliseconds for a provider-bound operation.
|
|
11
|
+
*
|
|
12
|
+
* An explicit value wins. Absent an explicit value, the deadline is the maximum
|
|
13
|
+
* of `floorMs` and the request-timeout limit that the provider advertises. Thus
|
|
14
|
+
* a configured request timeout can raise the floor. It cannot lower the floor.
|
|
15
|
+
*/
|
|
16
|
+
export function effectiveDeadlineMs(provider, floorMs, explicitMs) {
|
|
17
|
+
return explicitMs ?? Math.max(floorMs, provider.requestTimeoutMs ?? 0);
|
|
18
|
+
}
|