@opencode-ai/ai 0.0.0-dev-18409 → 0.0.0-dev-18412
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/image.js +5 -4
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.js +4 -6
- 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 +1 -1
- package/dist/protocols/open-responses.js +7 -16
- package/dist/protocols/openai-chat.js +23 -18
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +7 -3
- package/dist/protocols/shared.js +26 -13
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- 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 +31 -32
- package/dist/route/auth.js +3 -5
- 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 +2 -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 -88
- package/dist/schema/errors.js +36 -85
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Tool } from "@opencode-ai/schema/tool";
|
|
2
2
|
import { Effect, Schema, Stream } from "effect";
|
|
3
|
-
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
4
4
|
import { AIError, type ContentPart, type LLMRequest, type MediaPart, type ToolResultPart } from "../schema/index.js";
|
|
5
5
|
import { isRecord } from "../utils/record.js";
|
|
6
6
|
export { isRecord };
|
|
@@ -54,7 +54,7 @@ export declare const subtractTokens: (total: number | undefined, subtrahend: num
|
|
|
54
54
|
* (e.g. Anthropic, whose `input_tokens` is already non-cached only).
|
|
55
55
|
*/
|
|
56
56
|
export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined;
|
|
57
|
-
export declare const eventError: (route: string, message: string,
|
|
57
|
+
export declare const eventError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
|
|
58
58
|
export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>;
|
|
59
59
|
/**
|
|
60
60
|
* Join the `text` field of a list of parts with newlines. Used by routes
|
|
@@ -127,7 +127,11 @@ export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, eve
|
|
|
127
127
|
/**
|
|
128
128
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
129
129
|
*/
|
|
130
|
-
export declare const invalidRequest: (message: string) => AIError;
|
|
130
|
+
export declare const invalidRequest: (message: string, cause?: unknown) => AIError;
|
|
131
|
+
export declare const imageResponse: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
|
|
132
|
+
body: string;
|
|
133
|
+
invalid: (message: string, cause?: unknown) => AIError;
|
|
134
|
+
}, AIError, never>;
|
|
131
135
|
export declare const matchToolChoice: <Auto, None, Required, Tool>(route: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>, cases: {
|
|
132
136
|
readonly auto: () => Auto;
|
|
133
137
|
readonly none: () => None;
|
package/dist/protocols/shared.js
CHANGED
|
@@ -2,8 +2,8 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { Tool } from "@opencode-ai/schema/tool";
|
|
3
3
|
import { Effect, Schema, Stream } from "effect";
|
|
4
4
|
import * as Sse from "effect/unstable/encoding/Sse";
|
|
5
|
-
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
6
|
-
import {
|
|
5
|
+
import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
6
|
+
import { InvalidProviderOutputError, InvalidRequestError, AIError, HttpContext, } from "../schema/index.js";
|
|
7
7
|
import { isRecord } from "../utils/record.js";
|
|
8
8
|
export { isRecord };
|
|
9
9
|
export const Json = Schema.fromJsonString(Schema.Unknown);
|
|
@@ -72,14 +72,12 @@ export const sumTokens = (...values) => {
|
|
|
72
72
|
return undefined;
|
|
73
73
|
return values.reduce((acc, value) => acc + (value ?? 0), 0);
|
|
74
74
|
};
|
|
75
|
-
export const eventError = (route, message,
|
|
76
|
-
|
|
77
|
-
method: "stream",
|
|
78
|
-
reason: new InvalidProviderOutputReason({ route, message, raw }),
|
|
75
|
+
export const eventError = (route, message, body, cause) => new AIError({
|
|
76
|
+
reason: new InvalidProviderOutputError({ route, message, body, cause }),
|
|
79
77
|
});
|
|
80
78
|
export const parseJson = (route, input, message) => Effect.try({
|
|
81
79
|
try: () => decodeJson(input),
|
|
82
|
-
catch: () => eventError(route, message, input),
|
|
80
|
+
catch: (cause) => eventError(route, message, input, cause),
|
|
83
81
|
});
|
|
84
82
|
/**
|
|
85
83
|
* Join the `text` field of a list of parts with newlines. Used by routes
|
|
@@ -178,7 +176,7 @@ export const sseFraming = (bytes, events) => bytes.pipe(Stream.decodeText(), Str
|
|
|
178
176
|
}, (state, chunk) => Effect.gen(function* () {
|
|
179
177
|
const error = state.parser.feed(chunk);
|
|
180
178
|
if (error)
|
|
181
|
-
return yield* eventError("sse", error.message);
|
|
179
|
+
return yield* eventError("sse", error.message, chunk, error);
|
|
182
180
|
return [state, state.output.splice(0)];
|
|
183
181
|
})), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
|
|
184
182
|
event.data.length > 0 &&
|
|
@@ -186,10 +184,25 @@ export const sseFraming = (bytes, events) => bytes.pipe(Stream.decodeText(), Str
|
|
|
186
184
|
/**
|
|
187
185
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
188
186
|
*/
|
|
189
|
-
export const invalidRequest = (message) => new AIError({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
export const invalidRequest = (message, cause) => new AIError({
|
|
188
|
+
reason: new InvalidRequestError({ message, cause }),
|
|
189
|
+
});
|
|
190
|
+
export const imageResponse = Effect.fn("ProviderShared.imageResponse")(function* (route, name, response) {
|
|
191
|
+
const http = new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
|
|
192
|
+
const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
|
|
193
|
+
reason: new InvalidProviderOutputError({
|
|
194
|
+
route,
|
|
195
|
+
message: `Failed to read the ${name} response`,
|
|
196
|
+
http,
|
|
197
|
+
cause,
|
|
198
|
+
}),
|
|
199
|
+
})));
|
|
200
|
+
return {
|
|
201
|
+
body,
|
|
202
|
+
invalid: (message, cause) => new AIError({
|
|
203
|
+
reason: new InvalidProviderOutputError({ route, message, body, http, cause }),
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
193
206
|
});
|
|
194
207
|
export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () {
|
|
195
208
|
if (toolChoice.type === "auto")
|
|
@@ -217,7 +230,7 @@ export const unsupportedContent = (route, role, types) => invalidRequest(`${rout
|
|
|
217
230
|
* invalid(e.message)))`. Any decode error is translated into
|
|
218
231
|
* `AIError` carrying the original parse-error message.
|
|
219
232
|
*/
|
|
220
|
-
export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message)));
|
|
233
|
+
export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message, error)));
|
|
221
234
|
/**
|
|
222
235
|
* Build an HTTP POST with a JSON body. Sets `content-type: application/json`
|
|
223
236
|
* automatically after caller-supplied headers so routes cannot accidentally
|
|
@@ -4,18 +4,18 @@ import { AIError } from "../../schema/index.js";
|
|
|
4
4
|
export declare const dataUrl: (input: Extract<ImageInput, {
|
|
5
5
|
readonly type: "bytes";
|
|
6
6
|
}>) => string;
|
|
7
|
-
export declare const decodeDataUrl: (url: string
|
|
7
|
+
export declare const decodeDataUrl: (url: string) => Effect.Effect<{
|
|
8
8
|
readonly mediaType: string;
|
|
9
9
|
readonly data: Uint8Array;
|
|
10
10
|
} | undefined, AIError>;
|
|
11
|
-
export declare const invalidImageInput: (
|
|
11
|
+
export declare const invalidImageInput: (message: string, cause?: unknown) => AIError;
|
|
12
12
|
export declare const ImageInputs: {
|
|
13
13
|
readonly dataUrl: (input: Extract<ImageInput, {
|
|
14
14
|
readonly type: "bytes";
|
|
15
15
|
}>) => string;
|
|
16
|
-
readonly decodeDataUrl: (url: string
|
|
16
|
+
readonly decodeDataUrl: (url: string) => Effect.Effect<{
|
|
17
17
|
readonly mediaType: string;
|
|
18
18
|
readonly data: Uint8Array;
|
|
19
19
|
} | undefined, AIError>;
|
|
20
|
-
readonly invalid: (
|
|
20
|
+
readonly invalid: (message: string, cause?: unknown) => AIError;
|
|
21
21
|
};
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { Effect, Encoding } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
const invalid = (
|
|
4
|
-
|
|
5
|
-
method: "generate",
|
|
6
|
-
reason: new InvalidRequestReason({ message }),
|
|
2
|
+
import { InvalidRequestError, AIError } from "../../schema/index.js";
|
|
3
|
+
const invalid = (message, cause) => new AIError({
|
|
4
|
+
reason: new InvalidRequestError({ message, cause }),
|
|
7
5
|
});
|
|
8
6
|
export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`;
|
|
9
|
-
export const decodeDataUrl = (url
|
|
7
|
+
export const decodeDataUrl = (url) => {
|
|
10
8
|
if (!url.startsWith("data:"))
|
|
11
9
|
return Effect.undefined;
|
|
12
10
|
const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
|
|
13
11
|
if (!match)
|
|
14
|
-
return Effect.fail(invalid(
|
|
15
|
-
return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError(() => invalid(
|
|
12
|
+
return Effect.fail(invalid("Image data URLs must contain a MIME type and base64 data"));
|
|
13
|
+
return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError((cause) => invalid("Image data URL contains invalid base64 data", cause)), Effect.map((data) => ({ mediaType: match[1], data })));
|
|
16
14
|
};
|
|
17
15
|
export const invalidImageInput = invalid;
|
|
18
16
|
export const ImageInputs = {
|
|
@@ -2,7 +2,7 @@ import { Effect, Encoding, Schema } from "effect";
|
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { GeneratedImage, ImageModel, ImageResponse } from "../image.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { ProviderShared, optionalNull } from "./shared.js";
|
|
7
7
|
import { ImageInputs } from "./utils/image-input.js";
|
|
8
8
|
const ADAPTER = "xai-images";
|
|
@@ -28,11 +28,6 @@ const nativeOptions = (options) => {
|
|
|
28
28
|
...native,
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
-
const invalidOutput = (message) => new AIError({
|
|
32
|
-
module: ADAPTER,
|
|
33
|
-
method: "generate",
|
|
34
|
-
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
|
35
|
-
});
|
|
36
31
|
const applyQuery = (url, query) => {
|
|
37
32
|
if (!query)
|
|
38
33
|
return url;
|
|
@@ -55,7 +50,7 @@ export const model = (input) => {
|
|
|
55
50
|
return undefined;
|
|
56
51
|
});
|
|
57
52
|
if (imageReferences.some((image) => image === undefined))
|
|
58
|
-
return yield* ImageInputs.invalid(
|
|
53
|
+
return yield* ImageInputs.invalid("xAI Images accepts image URLs, data URLs, bytes, and file IDs");
|
|
59
54
|
const requestBody = mergeJsonRecords({
|
|
60
55
|
model: request.model.id,
|
|
61
56
|
prompt: request.prompt,
|
|
@@ -72,12 +67,12 @@ export const model = (input) => {
|
|
|
72
67
|
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
|
73
68
|
});
|
|
74
69
|
const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
|
|
75
|
-
const
|
|
76
|
-
const decoded = yield* Schema.decodeUnknownEffect(XAIImageResponse)(
|
|
70
|
+
const output = yield* ProviderShared.imageResponse(ADAPTER, "xAI Images", response);
|
|
71
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(XAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("xAI Images returned an invalid response", cause)));
|
|
77
72
|
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
78
73
|
const mediaType = item.mime_type ?? "application/octet-stream";
|
|
79
74
|
if (item.b64_json)
|
|
80
|
-
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() =>
|
|
75
|
+
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`xAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
|
|
81
76
|
mediaType,
|
|
82
77
|
data,
|
|
83
78
|
providerMetadata: item.revised_prompt === undefined || item.revised_prompt === null
|
|
@@ -92,10 +87,10 @@ export const model = (input) => {
|
|
|
92
87
|
? undefined
|
|
93
88
|
: { xai: { revisedPrompt: item.revised_prompt } },
|
|
94
89
|
}));
|
|
95
|
-
return Effect.fail(
|
|
90
|
+
return Effect.fail(output.invalid(`xAI Images result ${index} has neither image data nor a URL`));
|
|
96
91
|
});
|
|
97
92
|
if (images.length === 0)
|
|
98
|
-
return yield*
|
|
93
|
+
return yield* output.invalid("xAI Images returned no images");
|
|
99
94
|
const usage = ProviderShared.isRecord(decoded.usage) ? decoded.usage : undefined;
|
|
100
95
|
return new ImageResponse({
|
|
101
96
|
images,
|
|
@@ -2,7 +2,7 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { GeneratedImage, ImageModel, ImageResponse } from "../image.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { ProviderShared } from "./shared.js";
|
|
7
7
|
import { ImageInputs } from "./utils/image-input.js";
|
|
8
8
|
const ADAPTER = "zai-images";
|
|
@@ -27,11 +27,6 @@ const nativeOptions = (options) => {
|
|
|
27
27
|
...native,
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
|
-
const invalidOutput = (message) => new AIError({
|
|
31
|
-
module: ADAPTER,
|
|
32
|
-
method: "generate",
|
|
33
|
-
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
|
34
|
-
});
|
|
35
30
|
const applyQuery = (url, query) => {
|
|
36
31
|
if (!query)
|
|
37
32
|
return url;
|
|
@@ -44,7 +39,7 @@ export const model = (input) => {
|
|
|
44
39
|
id: ADAPTER,
|
|
45
40
|
generate: Effect.fn("ZAIImages.generate")(function* (request, execute) {
|
|
46
41
|
if ((request.images?.length ?? 0) > 0)
|
|
47
|
-
return yield* ImageInputs.invalid(
|
|
42
|
+
return yield* ImageInputs.invalid("Z.ai hosted image generation does not support image inputs");
|
|
48
43
|
const http = mergeHttpOptions(request.model.http, request.http);
|
|
49
44
|
const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body);
|
|
50
45
|
const text = ProviderShared.encodeJson(requestBody);
|
|
@@ -57,10 +52,10 @@ export const model = (input) => {
|
|
|
57
52
|
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
|
58
53
|
});
|
|
59
54
|
const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
|
|
60
|
-
const
|
|
61
|
-
const decoded = yield* Schema.decodeUnknownEffect(ZAIImageResponse)(
|
|
55
|
+
const output = yield* ProviderShared.imageResponse(ADAPTER, "Z.ai Images", response);
|
|
56
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ZAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Z.ai Images returned an invalid response", cause)));
|
|
62
57
|
if (decoded.data.length === 0)
|
|
63
|
-
return yield*
|
|
58
|
+
return yield* output.invalid("Z.ai Images returned no images");
|
|
64
59
|
return new ImageResponse({
|
|
65
60
|
images: decoded.data.map((item) => new GeneratedImage({
|
|
66
61
|
mediaType: "application/octet-stream",
|
package/dist/provider-error.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { AIError, type HttpContext, type HttpRateLimitDetails
|
|
1
|
+
import { AIError, type HttpContext, type HttpRateLimitDetails } from "./schema/index.js";
|
|
2
2
|
export declare const isContextOverflow: (message: string) => boolean;
|
|
3
3
|
export declare const isPayloadTooLarge: (message: string) => boolean;
|
|
4
4
|
export declare const isContextOverflowFailure: (failure: unknown) => boolean;
|
|
5
5
|
export interface ProviderFailure {
|
|
6
6
|
readonly message: string;
|
|
7
7
|
readonly status?: number | undefined;
|
|
8
|
-
readonly code?: string | undefined;
|
|
9
8
|
readonly rawBody?: string | undefined;
|
|
9
|
+
readonly data?: unknown;
|
|
10
|
+
readonly http?: HttpContext | undefined;
|
|
11
|
+
readonly cause?: unknown;
|
|
10
12
|
readonly retryAfterMs?: number | undefined;
|
|
11
13
|
readonly rateLimit?: HttpRateLimitDetails | undefined;
|
|
12
|
-
readonly http?: HttpContext | undefined;
|
|
13
|
-
readonly providerMetadata?: ProviderMetadata | undefined;
|
|
14
14
|
}
|
|
15
15
|
export declare function classifyProviderFailure(input: ProviderFailure): AIError["reason"];
|
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,
|
|
@@ -57,80 +57,79 @@ const NETWORK_ERROR_TEXT = /network[-_\s]error/i;
|
|
|
57
57
|
// Keep HTTP failures and provider-reported stream failures on one typed path so
|
|
58
58
|
// session retry policy never needs provider-specific string matching.
|
|
59
59
|
export function classifyProviderFailure(input) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
.map((code) => code.toLowerCase());
|
|
60
|
+
const details = { message: input.message, body: input.rawBody, http: input.http, cause: input.cause };
|
|
61
|
+
const body = input.rawBody ?? "";
|
|
62
|
+
const codes = [...providerCodes(input.data), ...providerCodes(body), ...providerCodes(input.message)].map((code) => code.toLowerCase());
|
|
64
63
|
// Scan the raw payload too so signals missing from the summary message
|
|
65
64
|
// (e.g. overflow phrases nested in a JSON error body) still classify.
|
|
66
65
|
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
66
|
const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500);
|
|
69
67
|
if (clientScoped &&
|
|
70
68
|
(codes.includes("context_length_exceeded") ||
|
|
71
69
|
codes.includes("model_context_window_exceeded") ||
|
|
72
70
|
codes.includes("request_too_large") ||
|
|
73
71
|
isContextOverflow(text)))
|
|
74
|
-
return new
|
|
72
|
+
return new InvalidRequestError({ ...details, classification: "context-overflow" });
|
|
75
73
|
if (input.status === 413 || isPayloadTooLarge(text))
|
|
76
|
-
return new
|
|
74
|
+
return new InvalidRequestError({ ...details, classification: "payload-too-large" });
|
|
77
75
|
if (CONTENT_POLICY_TEXT.test(text))
|
|
78
|
-
return new
|
|
76
|
+
return new ContentPolicyError(details);
|
|
79
77
|
if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
|
|
80
|
-
return new
|
|
78
|
+
return new QuotaExceededError(details);
|
|
81
79
|
if (input.status === 401)
|
|
82
|
-
return new
|
|
80
|
+
return new AuthenticationError({ ...details, kind: "invalid" });
|
|
83
81
|
if (input.status === 403)
|
|
84
|
-
return new
|
|
82
|
+
return new AuthenticationError({ ...details, kind: "insufficient-permissions" });
|
|
85
83
|
if (codes.includes("authentication_error"))
|
|
86
|
-
return new
|
|
84
|
+
return new AuthenticationError({ ...details, kind: "invalid" });
|
|
87
85
|
if (codes.includes("permission_error"))
|
|
88
|
-
return new
|
|
86
|
+
return new AuthenticationError({ ...details, kind: "insufficient-permissions" });
|
|
89
87
|
if (codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception"))
|
|
90
|
-
return new
|
|
91
|
-
...
|
|
88
|
+
return new RateLimitError({
|
|
89
|
+
...details,
|
|
92
90
|
retryAfterMs: input.retryAfterMs,
|
|
93
91
|
rateLimit: input.rateLimit,
|
|
94
92
|
});
|
|
95
93
|
if (RATE_LIMIT_TEXT.test(text))
|
|
96
|
-
return new
|
|
97
|
-
...
|
|
94
|
+
return new RateLimitError({
|
|
95
|
+
...details,
|
|
98
96
|
retryAfterMs: input.retryAfterMs,
|
|
99
97
|
rateLimit: input.rateLimit,
|
|
100
98
|
});
|
|
101
99
|
if (NETWORK_ERROR_TEXT.test(text))
|
|
102
|
-
return new
|
|
100
|
+
return new ProviderInternalError(details);
|
|
103
101
|
if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
|
|
104
|
-
return new
|
|
105
|
-
...
|
|
106
|
-
status: input.status,
|
|
102
|
+
return new ProviderInternalError({
|
|
103
|
+
...details,
|
|
107
104
|
retryAfterMs: input.retryAfterMs,
|
|
108
105
|
});
|
|
109
106
|
if (input.status === 429) {
|
|
110
|
-
return new
|
|
111
|
-
...
|
|
107
|
+
return new RateLimitError({
|
|
108
|
+
...details,
|
|
112
109
|
retryAfterMs: input.retryAfterMs,
|
|
113
110
|
rateLimit: input.rateLimit,
|
|
114
111
|
});
|
|
115
112
|
}
|
|
116
113
|
if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))
|
|
117
|
-
return new
|
|
118
|
-
...
|
|
119
|
-
status: input.status,
|
|
114
|
+
return new ProviderInternalError({
|
|
115
|
+
...details,
|
|
120
116
|
retryAfterMs: input.retryAfterMs,
|
|
121
117
|
});
|
|
122
118
|
if (codes.some((code) => INVALID_REQUEST_CODES.has(code)))
|
|
123
|
-
return new
|
|
119
|
+
return new InvalidRequestError(details);
|
|
124
120
|
if (input.status === 400 || input.status === 404 || input.status === 413 || input.status === 422)
|
|
125
|
-
return new
|
|
126
|
-
return new
|
|
121
|
+
return new InvalidRequestError(details);
|
|
122
|
+
return new UnknownProviderError(details);
|
|
127
123
|
}
|
|
128
124
|
function providerCodes(value) {
|
|
129
|
-
const decoded = Option.getOrUndefined(decodeJson(value));
|
|
125
|
+
const decoded = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value;
|
|
130
126
|
if (!isRecord(decoded))
|
|
131
127
|
return [];
|
|
132
128
|
const error = isRecord(decoded.error) ? decoded.error : undefined;
|
|
133
|
-
|
|
129
|
+
const response = isRecord(decoded.response) ? decoded.response : undefined;
|
|
130
|
+
const responseError = response && isRecord(response.error) ? response.error : undefined;
|
|
131
|
+
const exception = isRecord(decoded.exception) ? decoded.exception : undefined;
|
|
132
|
+
return [decoded.code, error?.code, error?.type, error?.status, responseError?.code, exception?.type].filter((value) => typeof value === "string");
|
|
134
133
|
}
|
|
135
134
|
function isRecord(value) {
|
|
136
135
|
return typeof value === "object" && value !== null;
|
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, kind: "missing" })
|
|
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.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>;
|