@opencode-ai/ai 0.0.0-beta-18387 → 0.0.0-beta-18593
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 +29 -11
- package/dist/image.js +5 -4
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.d.ts +1 -0
- package/dist/protocols/bedrock-converse.js +17 -7
- package/dist/protocols/bedrock-event-stream.js +16 -6
- package/dist/protocols/gemini.d.ts +1 -0
- package/dist/protocols/gemini.js +13 -1
- package/dist/protocols/google-images.js +8 -18
- package/dist/protocols/open-responses-channel.js +20 -9
- package/dist/protocols/open-responses-continuation.js +9 -8
- package/dist/protocols/open-responses.d.ts +9 -3
- package/dist/protocols/open-responses.js +139 -78
- package/dist/protocols/openai-chat.d.ts +3 -1
- package/dist/protocols/openai-chat.js +39 -28
- package/dist/protocols/openai-compatible-chat.js +1 -2
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +11 -7
- package/dist/protocols/shared.js +31 -18
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- package/dist/protocols/utils/lifecycle.d.ts +6 -2
- package/dist/protocols/utils/lifecycle.js +11 -7
- package/dist/protocols/utils/tool-stream.d.ts +6 -0
- package/dist/protocols/xai-images.js +7 -12
- package/dist/protocols/zai-images.js +5 -10
- package/dist/provider-error.d.ts +4 -4
- package/dist/provider-error.js +39 -55
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +1 -2
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -2
- package/dist/route/auth.js +3 -5
- package/dist/route/client.d.ts +2 -0
- package/dist/route/client.js +29 -11
- package/dist/route/executor.d.ts +9 -5
- package/dist/route/executor.js +33 -66
- package/dist/route/framing.d.ts +6 -2
- package/dist/route/framing.js +5 -0
- package/dist/route/transport/http.js +7 -2
- package/dist/route/transport/index.d.ts +3 -1
- package/dist/route/transport/websocket-channel.d.ts +2 -1
- package/dist/route/transport/websocket.d.ts +2 -1
- package/dist/route/transport/websocket.js +70 -30
- package/dist/schema/errors.d.ts +71 -89
- package/dist/schema/errors.js +35 -87
- package/dist/schema/events.d.ts +2835 -351
- package/dist/schema/events.js +32 -14
- package/dist/testing.d.ts +41 -2
- package/dist/testing.js +39 -18
- package/package.json +5 -5
package/dist/provider-error.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Option, Schema } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { AuthenticationError, ContentPolicyError, InvalidRequestError, AIError, ProviderErrorEvent, ProviderInternalError, QuotaExceededError, RateLimitError, UnknownProviderError, } from "./schema/index.js";
|
|
3
3
|
const patterns = [
|
|
4
4
|
/prompt is too long/i,
|
|
5
5
|
/input is too long for requested model/i,
|
|
@@ -24,6 +24,7 @@ const patterns = [
|
|
|
24
24
|
/too large for model with \d+ maximum context length/i,
|
|
25
25
|
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
|
|
26
26
|
/model_context_window_exceeded/i,
|
|
27
|
+
/range of input length should be/i,
|
|
27
28
|
/too many tokens/i,
|
|
28
29
|
/token limit exceeded/i,
|
|
29
30
|
/request_too_large/i,
|
|
@@ -38,6 +39,7 @@ export const isContextOverflowFailure = (failure) => failure instanceof AIError
|
|
|
38
39
|
: Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow";
|
|
39
40
|
const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
|
|
40
41
|
const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"]);
|
|
42
|
+
const AUTH_CODES = new Set(["authentication_error", "permission_error"]);
|
|
41
43
|
const SERVER_CODES = new Set([
|
|
42
44
|
"api_error",
|
|
43
45
|
"internal_error",
|
|
@@ -53,84 +55,66 @@ const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error"
|
|
|
53
55
|
const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i;
|
|
54
56
|
const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i;
|
|
55
57
|
const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i;
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
58
|
+
// Classification records affirmative evidence about a failure. Deterministic
|
|
59
|
+
// failures need positive identification (a 4xx status, quota/auth/policy
|
|
60
|
+
// signals); anything unrecognized stays UnknownProvider, which the session
|
|
61
|
+
// retry policy treats as retry-eligible because transient failures arrive in
|
|
62
|
+
// unpredictable shapes while deterministic rejections almost always carry a
|
|
63
|
+
// status or known code.
|
|
59
64
|
export function classifyProviderFailure(input) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
.map((code) => code.toLowerCase());
|
|
65
|
+
const details = { message: input.message, body: input.rawBody, http: input.http, cause: input.cause };
|
|
66
|
+
const body = input.rawBody ?? "";
|
|
67
|
+
const codes = [...providerCodes(input.data), ...providerCodes(body), ...providerCodes(input.message)].map((code) => code.toLowerCase());
|
|
64
68
|
// Scan the raw payload too so signals missing from the summary message
|
|
65
69
|
// (e.g. overflow phrases nested in a JSON error body) still classify.
|
|
66
70
|
const text = [input.message, body].filter((value) => value.length > 0).join("\n");
|
|
67
|
-
const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http };
|
|
68
71
|
const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500);
|
|
69
72
|
if (clientScoped &&
|
|
70
73
|
(codes.includes("context_length_exceeded") ||
|
|
71
74
|
codes.includes("model_context_window_exceeded") ||
|
|
72
75
|
codes.includes("request_too_large") ||
|
|
73
76
|
isContextOverflow(text)))
|
|
74
|
-
return new
|
|
77
|
+
return new InvalidRequestError({ ...details, classification: "context-overflow" });
|
|
75
78
|
if (input.status === 413 || isPayloadTooLarge(text))
|
|
76
|
-
return new
|
|
79
|
+
return new InvalidRequestError({ ...details, classification: "payload-too-large" });
|
|
77
80
|
if (CONTENT_POLICY_TEXT.test(text))
|
|
78
|
-
return new
|
|
81
|
+
return new ContentPolicyError(details);
|
|
79
82
|
if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
|
|
80
|
-
return new
|
|
81
|
-
if (input.status === 401)
|
|
82
|
-
return new
|
|
83
|
-
if (input.status ===
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return new
|
|
87
|
-
|
|
88
|
-
return new AuthenticationReason({ ...common, kind: "insufficient-permissions" });
|
|
89
|
-
if (codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception"))
|
|
90
|
-
return new RateLimitReason({
|
|
91
|
-
...common,
|
|
83
|
+
return new QuotaExceededError(details);
|
|
84
|
+
if (input.status === 401 || input.status === 403 || codes.some((code) => AUTH_CODES.has(code)))
|
|
85
|
+
return new AuthenticationError(details);
|
|
86
|
+
if (input.status === 429 ||
|
|
87
|
+
codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception") ||
|
|
88
|
+
RATE_LIMIT_TEXT.test(text))
|
|
89
|
+
return new RateLimitError({
|
|
90
|
+
...details,
|
|
92
91
|
retryAfterMs: input.retryAfterMs,
|
|
93
92
|
rateLimit: input.rateLimit,
|
|
94
93
|
});
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (NETWORK_ERROR_TEXT.test(text))
|
|
102
|
-
return new ProviderInternalReason({ ...common, status: input.status });
|
|
103
|
-
if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
|
|
104
|
-
return new ProviderInternalReason({
|
|
105
|
-
...common,
|
|
106
|
-
status: input.status,
|
|
107
|
-
retryAfterMs: input.retryAfterMs,
|
|
108
|
-
});
|
|
109
|
-
if (input.status === 429) {
|
|
110
|
-
return new RateLimitReason({
|
|
111
|
-
...common,
|
|
112
|
-
retryAfterMs: input.retryAfterMs,
|
|
113
|
-
rateLimit: input.rateLimit,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))
|
|
117
|
-
return new ProviderInternalReason({
|
|
118
|
-
...common,
|
|
119
|
-
status: input.status,
|
|
94
|
+
if (input.status === 408 ||
|
|
95
|
+
input.status === 409 ||
|
|
96
|
+
(input.status !== undefined && input.status >= 500) ||
|
|
97
|
+
codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
|
|
98
|
+
return new ProviderInternalError({
|
|
99
|
+
...details,
|
|
120
100
|
retryAfterMs: input.retryAfterMs,
|
|
121
101
|
});
|
|
122
102
|
if (codes.some((code) => INVALID_REQUEST_CODES.has(code)))
|
|
123
|
-
return new
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
103
|
+
return new InvalidRequestError(details);
|
|
104
|
+
// Any remaining 4xx is a deterministic rejection of this request.
|
|
105
|
+
if (input.status !== undefined && input.status >= 400 && input.status < 500)
|
|
106
|
+
return new InvalidRequestError(details);
|
|
107
|
+
return new UnknownProviderError(details);
|
|
127
108
|
}
|
|
128
109
|
function providerCodes(value) {
|
|
129
|
-
const decoded = Option.getOrUndefined(decodeJson(value));
|
|
110
|
+
const decoded = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value;
|
|
130
111
|
if (!isRecord(decoded))
|
|
131
112
|
return [];
|
|
132
113
|
const error = isRecord(decoded.error) ? decoded.error : undefined;
|
|
133
|
-
|
|
114
|
+
const response = isRecord(decoded.response) ? decoded.response : undefined;
|
|
115
|
+
const responseError = response && isRecord(response.error) ? response.error : undefined;
|
|
116
|
+
const exception = isRecord(decoded.exception) ? decoded.exception : undefined;
|
|
117
|
+
return [decoded.code, error?.code, error?.type, error?.status, responseError?.code, exception?.type].filter((value) => typeof value === "string");
|
|
134
118
|
}
|
|
135
119
|
function isRecord(value) {
|
|
136
120
|
return typeof value === "object" && value !== null;
|
package/dist/providers/groq.d.ts
CHANGED
|
@@ -125,7 +125,7 @@ export declare const protocol: Protocol<{
|
|
|
125
125
|
readonly parallel_tool_calls?: boolean | undefined;
|
|
126
126
|
readonly reasoning_format?: "parsed" | undefined;
|
|
127
127
|
readonly include_reasoning?: boolean | undefined;
|
|
128
|
-
}, string, {
|
|
128
|
+
}, string, "[DONE]" | {
|
|
129
129
|
readonly [x: string]: unknown;
|
|
130
130
|
readonly error?: {
|
|
131
131
|
readonly [x: string]: unknown;
|
package/dist/providers/groq.js
CHANGED
|
@@ -4,7 +4,6 @@ import { ProviderShared } from "../protocols/shared.js";
|
|
|
4
4
|
import { AuthOptions } from "../route/auth-options.js";
|
|
5
5
|
import { Route } from "../route/client.js";
|
|
6
6
|
import { Endpoint } from "../route/endpoint.js";
|
|
7
|
-
import { Framing } from "../route/framing.js";
|
|
8
7
|
import { Protocol } from "../route/protocol.js";
|
|
9
8
|
import { ProviderID } from "../schema/index.js";
|
|
10
9
|
import { profiles } from "./openai-compatible-profile.js";
|
|
@@ -47,7 +46,7 @@ export const route = Route.make({
|
|
|
47
46
|
providerMetadataKey: "openai",
|
|
48
47
|
protocol,
|
|
49
48
|
endpoint: Endpoint.path("/chat/completions", { baseURL: profiles.groq.baseURL }),
|
|
50
|
-
framing:
|
|
49
|
+
framing: OpenAIChat.framing,
|
|
51
50
|
});
|
|
52
51
|
export const configure = (input = {}) => {
|
|
53
52
|
const { apiKey: _apiKey, auth: _auth, baseURL, ...defaults } = input;
|
|
@@ -282,7 +282,7 @@ export declare const protocol: Protocol<{
|
|
|
282
282
|
readonly tool_stream?: boolean | undefined;
|
|
283
283
|
readonly frequency_penalty?: number | undefined;
|
|
284
284
|
readonly presence_penalty?: number | undefined;
|
|
285
|
-
}, string, {
|
|
285
|
+
}, string, "[DONE]" | {
|
|
286
286
|
readonly [x: string]: unknown;
|
|
287
287
|
readonly error?: {
|
|
288
288
|
readonly [x: string]: unknown;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { Route } from "../route/client.js";
|
|
3
3
|
import { Endpoint } from "../route/endpoint.js";
|
|
4
|
-
import { Framing } from "../route/framing.js";
|
|
5
4
|
import { Protocol } from "../route/protocol.js";
|
|
6
5
|
import { AuthOptions } from "../route/auth-options.js";
|
|
7
6
|
import { ProviderID } from "../schema/index.js";
|
|
@@ -87,7 +86,7 @@ export const route = Route.make({
|
|
|
87
86
|
providerMetadataKey: "openrouter",
|
|
88
87
|
protocol,
|
|
89
88
|
endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }),
|
|
90
|
-
framing:
|
|
89
|
+
framing: OpenAIChat.framing,
|
|
91
90
|
});
|
|
92
91
|
export const routes = [route];
|
|
93
92
|
const configuredRoute = (input) => {
|
package/dist/route/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Config, Effect, Redacted } from "effect";
|
|
2
2
|
import { Headers } from "effect/unstable/http";
|
|
3
|
-
import {
|
|
3
|
+
import { AuthenticationError, InvalidRequestError, AIError } from "../schema/index.js";
|
|
4
4
|
export class MissingCredentialError extends Error {
|
|
5
5
|
source;
|
|
6
6
|
_tag = "MissingCredentialError";
|
|
@@ -76,11 +76,9 @@ export function bearerHeader(name, source) {
|
|
|
76
76
|
const toAIError = (error) => {
|
|
77
77
|
if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
|
|
78
78
|
return new AIError({
|
|
79
|
-
module: "Auth",
|
|
80
|
-
method: "apply",
|
|
81
79
|
reason: error instanceof MissingCredentialError
|
|
82
|
-
? new
|
|
83
|
-
: new
|
|
80
|
+
? new AuthenticationError({ message: error.message, cause: error })
|
|
81
|
+
: new InvalidRequestError({ message: `Failed to resolve auth config: ${error.message}`, cause: error }),
|
|
84
82
|
});
|
|
85
83
|
}
|
|
86
84
|
return error;
|
package/dist/route/client.d.ts
CHANGED
|
@@ -173,6 +173,7 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
|
|
|
173
173
|
} | {
|
|
174
174
|
readonly id: string;
|
|
175
175
|
readonly type: "text-end";
|
|
176
|
+
readonly text?: string | undefined;
|
|
176
177
|
readonly providerMetadata?: {
|
|
177
178
|
readonly [x: string]: {
|
|
178
179
|
readonly [x: string]: unknown;
|
|
@@ -198,6 +199,7 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
|
|
|
198
199
|
} | {
|
|
199
200
|
readonly id: string;
|
|
200
201
|
readonly type: "reasoning-end";
|
|
202
|
+
readonly text?: string | undefined;
|
|
201
203
|
readonly providerMetadata?: {
|
|
202
204
|
readonly [x: string]: {
|
|
203
205
|
readonly [x: string]: unknown;
|
package/dist/route/client.js
CHANGED
|
@@ -7,7 +7,7 @@ import { HttpTransport } from "./transport/index.js";
|
|
|
7
7
|
import { applyCachePolicy } from "../cache-policy.js";
|
|
8
8
|
import { sanitizeSurrogates } from "../utils/sanitize.js";
|
|
9
9
|
import * as ProviderShared from "../protocols/shared.js";
|
|
10
|
-
import { AIError, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent,
|
|
10
|
+
import { AIError, AIErrorReason, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputError, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
|
|
11
11
|
const makeRouteLanguageModel = (route, mapped) => {
|
|
12
12
|
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
|
|
13
13
|
if (!provider)
|
|
@@ -60,14 +60,12 @@ const streamError = (route, message, cause) => {
|
|
|
60
60
|
const failed = cause.reasons.find(Cause.isFailReason)?.error;
|
|
61
61
|
if (failed instanceof AIError)
|
|
62
62
|
return failed;
|
|
63
|
-
return ProviderShared.eventError(route, message,
|
|
63
|
+
return ProviderShared.eventError(route, message, undefined, cause);
|
|
64
64
|
};
|
|
65
65
|
const incompleteStreamError = (route) => new AIError({
|
|
66
|
-
|
|
67
|
-
method: "stream",
|
|
68
|
-
reason: new InvalidProviderOutputReason({
|
|
69
|
-
classification: "incomplete-stream",
|
|
66
|
+
reason: new InvalidProviderOutputError({
|
|
70
67
|
message: "The provider response ended unexpectedly.",
|
|
68
|
+
classification: "incomplete-stream",
|
|
71
69
|
route,
|
|
72
70
|
}),
|
|
73
71
|
});
|
|
@@ -85,7 +83,7 @@ function makeFromTransport(input) {
|
|
|
85
83
|
const protocol = input.protocol;
|
|
86
84
|
const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema));
|
|
87
85
|
const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event);
|
|
88
|
-
const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))));
|
|
86
|
+
const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame), cause)));
|
|
89
87
|
const build = (routeInput) => {
|
|
90
88
|
const route = {
|
|
91
89
|
id: routeInput.id,
|
|
@@ -127,18 +125,38 @@ function makeFromTransport(input) {
|
|
|
127
125
|
streamPrepared: (prepared, request, runtime, options) => {
|
|
128
126
|
const route = `${request.model.provider}/${request.model.route.id}`;
|
|
129
127
|
return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => {
|
|
130
|
-
const
|
|
128
|
+
const terminal = protocol.stream.terminal;
|
|
129
|
+
// Preserve assembled inputs; replace only serialized event fallbacks with their original wire data.
|
|
130
|
+
const frameError = (frame, event = frame) => (error) => new AIError({
|
|
131
|
+
reason: AIErrorReason.make({
|
|
132
|
+
...error.reason,
|
|
133
|
+
message: error.reason.message,
|
|
134
|
+
cause: error.reason.cause,
|
|
135
|
+
body: error.reason.body !== undefined && error.reason.body !== ProviderShared.encodeJson(event)
|
|
136
|
+
? error.reason.body
|
|
137
|
+
: (execution.body?.(frame) ??
|
|
138
|
+
(typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))),
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
const events = execution.frames.pipe(Stream.mapEffect((frame) => decodeEvent(route)(frame).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to decode ${route} event`, cause))), Effect.map((event) => ({ event, frame })), Effect.mapError(frameError(frame)))), terminal ? Stream.takeUntil(({ event }) => terminal(event)) : (stream) => stream);
|
|
131
142
|
const stream = Stream.suspend(() => {
|
|
132
143
|
let state = protocol.stream.initial(request);
|
|
133
|
-
const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => {
|
|
144
|
+
const parsed = events.pipe(Stream.mapEffect(({ event, frame }) => protocol.stream.step(state, event).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to parse ${route} event`, cause))), Effect.map(([next, output]) => {
|
|
134
145
|
state = next;
|
|
135
146
|
return output;
|
|
136
|
-
}))), Stream.flatMap(Stream.fromIterable));
|
|
147
|
+
}), Effect.mapError(frameError(frame, event)))), Stream.flatMap(Stream.fromIterable));
|
|
137
148
|
const onHalt = protocol.stream.onHalt;
|
|
138
149
|
return onHalt
|
|
139
150
|
? parsed.pipe(Stream.concat(Stream.suspend(() => Stream.unwrap(onHalt(state).pipe(Effect.map(Stream.fromIterable))))))
|
|
140
151
|
: parsed;
|
|
141
|
-
}).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route))
|
|
152
|
+
}).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route), Stream.mapError((error) => new AIError({
|
|
153
|
+
reason: AIErrorReason.make({
|
|
154
|
+
...error.reason,
|
|
155
|
+
message: error.reason.message,
|
|
156
|
+
cause: error.reason.cause,
|
|
157
|
+
http: error.reason.http ?? execution.http,
|
|
158
|
+
}),
|
|
159
|
+
})));
|
|
142
160
|
return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream;
|
|
143
161
|
})));
|
|
144
162
|
},
|
package/dist/route/executor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Stream } from "effect";
|
|
2
2
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
3
|
-
import {
|
|
3
|
+
import { HttpContext, AIError } from "../schema/index.js";
|
|
4
4
|
export interface Interface {
|
|
5
5
|
readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
|
|
6
6
|
}
|
|
@@ -9,14 +9,18 @@ export type HttpMiddleware = (request: HttpClientRequest.HttpClientRequest, hand
|
|
|
9
9
|
declare const Service_base: Context.ServiceClass<Service, "@opencode/AI/RequestExecutor", Interface>;
|
|
10
10
|
export declare class Service extends Service_base {
|
|
11
11
|
}
|
|
12
|
-
export declare const
|
|
12
|
+
export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
|
|
13
|
+
/** Preserve HTTP diagnostics for executor and externally captured failures alike. */
|
|
14
|
+
export declare const httpFailure: (input: {
|
|
13
15
|
readonly message: string;
|
|
14
|
-
readonly url
|
|
16
|
+
readonly url?: string | undefined;
|
|
15
17
|
readonly status?: number | undefined;
|
|
16
|
-
readonly
|
|
18
|
+
readonly data?: unknown;
|
|
17
19
|
readonly responseHeaders?: Record<string, string> | undefined;
|
|
18
20
|
readonly responseBody?: string | undefined;
|
|
19
|
-
|
|
21
|
+
readonly cause?: unknown;
|
|
22
|
+
}) => AIError;
|
|
23
|
+
export declare const responseStream: (response: HttpClientResponse.HttpClientResponse) => Stream.Stream<Uint8Array, AIError>;
|
|
20
24
|
export declare const stream: (executor: Interface, request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Stream.Stream<Uint8Array, AIError>;
|
|
21
25
|
export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>;
|
|
22
26
|
export declare const fetchLayer: Layer.Layer<Service, never, never>;
|
package/dist/route/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
|
|
3
|
-
import { HttpContext, HttpRateLimitDetails,
|
|
3
|
+
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
|
|
4
4
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
5
5
|
export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {
|
|
6
6
|
}
|
|
@@ -60,20 +60,11 @@ const rateLimitDetails = (headers, retryAfter) => {
|
|
|
60
60
|
reset: Object.keys(reset).length === 0 ? undefined : reset,
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
url: request.url,
|
|
66
|
-
headers: headerDetails(request.headers),
|
|
67
|
-
});
|
|
68
|
-
const responseDetails = (response) => new HttpResponseDetails({
|
|
63
|
+
export const responseHttp = (response) => new HttpContext({
|
|
64
|
+
url: response.request.url,
|
|
69
65
|
status: response.status,
|
|
70
66
|
headers: headerDetails(response.headers),
|
|
71
67
|
});
|
|
72
|
-
const responseBody = (body) => {
|
|
73
|
-
if (body === undefined)
|
|
74
|
-
return {};
|
|
75
|
-
return { body };
|
|
76
|
-
};
|
|
77
68
|
const decodeProviderBody = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({
|
|
78
69
|
message: Schema.optionalKey(Schema.String),
|
|
79
70
|
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
|
|
@@ -83,60 +74,36 @@ const providerMessage = (status, body) => {
|
|
|
83
74
|
return ([decoded?.error?.message, decoded?.message].find((message) => message?.trim()) ??
|
|
84
75
|
`Provider request failed with HTTP ${status}`);
|
|
85
76
|
};
|
|
86
|
-
const
|
|
87
|
-
request: requestDetails(input.request),
|
|
88
|
-
response: responseDetails(input.response),
|
|
89
|
-
...input.body,
|
|
90
|
-
rateLimit: input.rateLimit,
|
|
91
|
-
});
|
|
92
|
-
const statusError = (request) => (response) => Effect.gen(function* () {
|
|
77
|
+
const statusError = (response) => Effect.gen(function* () {
|
|
93
78
|
if (response.status < 400)
|
|
94
79
|
return response;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
reason: classifyProviderFailure({
|
|
104
|
-
status: response.status,
|
|
105
|
-
message: providerMessage(response.status, body),
|
|
106
|
-
retryAfterMs: retryAfter,
|
|
107
|
-
rateLimit,
|
|
108
|
-
http: responseHttp({
|
|
109
|
-
request,
|
|
110
|
-
response,
|
|
111
|
-
body: details,
|
|
112
|
-
rateLimit,
|
|
113
|
-
}),
|
|
114
|
-
}),
|
|
80
|
+
const result = yield* response.text.pipe(Effect.result);
|
|
81
|
+
return yield* httpFailure({
|
|
82
|
+
message: providerMessage(response.status, result._tag === "Success" ? result.success : undefined),
|
|
83
|
+
url: response.request.url,
|
|
84
|
+
status: response.status,
|
|
85
|
+
responseHeaders: headerDetails(response.headers),
|
|
86
|
+
responseBody: result._tag === "Success" ? result.success : undefined,
|
|
87
|
+
cause: result._tag === "Failure" ? (result.failure.cause ?? result.failure) : undefined,
|
|
115
88
|
});
|
|
116
89
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// executor-driven requests produce. The originating request is not available on
|
|
120
|
-
// that path, so the method is assumed (language model calls are always POST),
|
|
121
|
-
// request headers are empty.
|
|
122
|
-
export const classifyHttpFailure = (input) => {
|
|
90
|
+
/** Preserve HTTP diagnostics for executor and externally captured failures alike. */
|
|
91
|
+
export const httpFailure = (input) => {
|
|
123
92
|
const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders));
|
|
124
93
|
const retryAfter = retryAfterMs(headers);
|
|
125
94
|
const rateLimit = rateLimitDetails(headers, retryAfter);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
http: new HttpContext({
|
|
134
|
-
request: new HttpRequestDetails({ method: "POST", url: input.url, headers: {} }),
|
|
135
|
-
response: input.status === undefined
|
|
136
|
-
? undefined
|
|
137
|
-
: new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }),
|
|
138
|
-
...details,
|
|
95
|
+
return new AIError({
|
|
96
|
+
reason: classifyProviderFailure({
|
|
97
|
+
message: input.message,
|
|
98
|
+
status: input.status,
|
|
99
|
+
data: input.data,
|
|
100
|
+
rawBody: input.responseBody,
|
|
101
|
+
retryAfterMs: retryAfter,
|
|
139
102
|
rateLimit,
|
|
103
|
+
cause: input.cause,
|
|
104
|
+
http: input.status === undefined || input.url === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: new HttpContext({ url: input.url, status: input.status, headers }),
|
|
140
107
|
}),
|
|
141
108
|
});
|
|
142
109
|
};
|
|
@@ -160,19 +127,18 @@ const nativeTransportFailure = (error) => {
|
|
|
160
127
|
const httpError = (input) => {
|
|
161
128
|
const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request;
|
|
162
129
|
const transportError = (failure) => new AIError({
|
|
163
|
-
|
|
164
|
-
method: input.operation,
|
|
165
|
-
reason: new TransportReason({
|
|
130
|
+
reason: new TransportError({
|
|
166
131
|
message: failure.message,
|
|
132
|
+
cause: source,
|
|
133
|
+
http: input.http,
|
|
167
134
|
transport: "http",
|
|
168
135
|
operation: input.operation,
|
|
169
136
|
code: failure.code,
|
|
170
137
|
url: request.url,
|
|
171
|
-
http: new HttpContext({ request: requestDetails(request) }),
|
|
172
138
|
}),
|
|
173
139
|
});
|
|
174
140
|
const source = HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason
|
|
175
|
-
? input.error.reason.cause
|
|
141
|
+
? (input.error.reason.cause ?? input.error)
|
|
176
142
|
: input.error;
|
|
177
143
|
const native = nativeTransportFailure(source);
|
|
178
144
|
const code = native?.code;
|
|
@@ -194,19 +160,20 @@ const httpError = (input) => {
|
|
|
194
160
|
code: code ?? input.error.reason._tag,
|
|
195
161
|
});
|
|
196
162
|
};
|
|
163
|
+
export const responseStream = (response) => response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read", http: responseHttp(response) })));
|
|
197
164
|
export const stream = (executor, request, middleware) => Stream.unwrap(Effect.gen(function* () {
|
|
198
165
|
const response = yield* executor.execute(request, middleware);
|
|
199
|
-
return
|
|
166
|
+
return responseStream(response);
|
|
200
167
|
}));
|
|
201
168
|
export const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
202
169
|
const http = yield* HttpClient.HttpClient;
|
|
203
170
|
const executeOnce = (request, middleware) => Effect.gen(function* () {
|
|
204
171
|
if (!middleware)
|
|
205
|
-
return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError
|
|
172
|
+
return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError));
|
|
206
173
|
const response = yield* middleware(request, (input) => http
|
|
207
174
|
.execute(input)
|
|
208
175
|
.pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })));
|
|
209
|
-
return yield* statusError(response
|
|
176
|
+
return yield* statusError(response);
|
|
210
177
|
});
|
|
211
178
|
return Service.of({
|
|
212
179
|
execute: executeOnce,
|
package/dist/route/framing.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { AIError } from "../schema/index.js";
|
|
|
6
6
|
* `Framing` is the byte-stream-shaped seam between transport and protocol:
|
|
7
7
|
*
|
|
8
8
|
* - SSE (`Framing.sse`) — UTF-8 decode the body, run the SSE channel decoder,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* and emit the `data:` payload of each non-empty event. The default drops
|
|
10
|
+
* `[DONE]`; protocols that use it as a terminal select `sseWithDone`.
|
|
11
11
|
* - AWS event stream — length-prefixed binary frames with CRC checksums.
|
|
12
12
|
* Each emitted frame is one parsed binary event record.
|
|
13
13
|
*
|
|
@@ -17,9 +17,13 @@ import type { AIError } from "../schema/index.js";
|
|
|
17
17
|
export interface Definition<Frame> {
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>;
|
|
20
|
+
/** Original wire representation when framing transforms the provider payload. */
|
|
21
|
+
readonly body?: (frame: Frame) => string | undefined;
|
|
20
22
|
}
|
|
21
23
|
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
|
22
24
|
export declare const sse: Definition<string>;
|
|
25
|
+
/** Server-Sent Events framing that retains the conventional `[DONE]` sentinel. */
|
|
26
|
+
export declare const sseWithDone: Definition<string>;
|
|
23
27
|
/** SSE framing restricted to protocol-recognized event names. */
|
|
24
28
|
export declare const sseEvents: (events: ReadonlySet<string>) => Definition<string>;
|
|
25
29
|
export * as Framing from "./framing.js";
|
package/dist/route/framing.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as ProviderShared from "../protocols/shared.js";
|
|
2
2
|
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
|
3
3
|
export const sse = { id: "sse", frame: ProviderShared.sseFraming };
|
|
4
|
+
/** Server-Sent Events framing that retains the conventional `[DONE]` sentinel. */
|
|
5
|
+
export const sseWithDone = {
|
|
6
|
+
id: "sse",
|
|
7
|
+
frame: (bytes) => ProviderShared.sseFraming(bytes, undefined, true),
|
|
8
|
+
};
|
|
4
9
|
/** SSE framing restricted to protocol-recognized event names. */
|
|
5
10
|
export const sseEvents = (events) => ({
|
|
6
11
|
id: "sse",
|
|
@@ -53,8 +53,13 @@ export const httpJson = (input) => ({
|
|
|
53
53
|
middleware: prepareInput.middleware,
|
|
54
54
|
};
|
|
55
55
|
}),
|
|
56
|
-
execute: (prepared, _request, runtime) => Effect.
|
|
57
|
-
|
|
56
|
+
execute: (prepared, _request, runtime) => Effect.gen(function* () {
|
|
57
|
+
const response = yield* runtime.http.execute(prepared.request, prepared.middleware);
|
|
58
|
+
return {
|
|
59
|
+
frames: prepared.framing.frame(RequestExecutor.responseStream(response)),
|
|
60
|
+
http: RequestExecutor.responseHttp(response),
|
|
61
|
+
body: prepared.framing.body,
|
|
62
|
+
};
|
|
58
63
|
}),
|
|
59
64
|
});
|
|
60
65
|
export const sseJson = {
|
|
@@ -3,12 +3,14 @@ import { Endpoint } from "../endpoint.js";
|
|
|
3
3
|
import { Auth } from "../auth.js";
|
|
4
4
|
import type { HttpMiddleware, Interface as RequestExecutorInterface } from "../executor.js";
|
|
5
5
|
import type { WebSocketChannelExecutor } from "./websocket-channel.js";
|
|
6
|
-
import type { AIError, LLMRequest } from "../../schema/index.js";
|
|
6
|
+
import type { AIError, HttpContext, LLMRequest } from "../../schema/index.js";
|
|
7
7
|
export interface TransportRuntime {
|
|
8
8
|
readonly http: RequestExecutorInterface;
|
|
9
9
|
}
|
|
10
10
|
export interface TransportExecution<Frame> {
|
|
11
11
|
readonly frames: Stream.Stream<Frame, AIError>;
|
|
12
|
+
readonly http?: HttpContext;
|
|
13
|
+
body?(frame: Frame): string | undefined;
|
|
12
14
|
/** Optional successful-consumption acknowledgement. HTTP leaves this absent. */
|
|
13
15
|
readonly complete?: Effect.Effect<void>;
|
|
14
16
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Effect, Scope, Stream } from "effect";
|
|
2
2
|
import type { Headers } from "effect/unstable/http";
|
|
3
|
-
import type { AIError } from "../../schema/index.js";
|
|
3
|
+
import type { AIError, HttpContext } from "../../schema/index.js";
|
|
4
4
|
export interface WebSocketChannelExecutor {
|
|
5
5
|
readonly execute: (exchange: WebSocketChannelExchange) => Effect.Effect<WebSocketChannelExecution, AIError, Scope.Scope>;
|
|
6
6
|
}
|
|
7
7
|
export interface WebSocketChannelExecution {
|
|
8
8
|
readonly frames: Stream.Stream<string, AIError>;
|
|
9
|
+
readonly http?: HttpContext;
|
|
9
10
|
/** Commits staged state after the decoded Route stream ends successfully. */
|
|
10
11
|
readonly complete: Effect.Effect<void>;
|
|
11
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
2
|
import { Headers } from "effect/unstable/http";
|
|
3
3
|
import { Socket } from "effect/unstable/socket";
|
|
4
|
-
import { AIError } from "../../schema/index.js";
|
|
4
|
+
import { AIError, type HttpContext } from "../../schema/index.js";
|
|
5
5
|
import type { Transport } from "./index.js";
|
|
6
6
|
import type { WebSocketChannelExecutor } from "./websocket-channel.js";
|
|
7
7
|
export interface WebSocketRequest {
|
|
@@ -9,6 +9,7 @@ export interface WebSocketRequest {
|
|
|
9
9
|
readonly headers: Headers.Headers;
|
|
10
10
|
}
|
|
11
11
|
export interface WebSocketConnection {
|
|
12
|
+
readonly http?: HttpContext;
|
|
12
13
|
readonly sendText: (message: string) => Effect.Effect<void, AIError>;
|
|
13
14
|
readonly messages: Stream.Stream<string | Uint8Array, AIError>;
|
|
14
15
|
readonly close: Effect.Effect<void, never>;
|