@opencode/ai 2.0.13 → 2.0.15
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 +115 -56
- package/dist/experimental/evaluation-client.d.ts +1 -1
- package/dist/experimental/evaluation-client.js +39 -3
- package/dist/experimental/evaluation.d.ts +4 -4
- package/dist/experimental/evaluation.js +2 -2
- package/dist/experimental/system-one.d.ts +3 -3
- package/dist/experimental/system-one.js +40 -51
- package/dist/generation.d.ts +69 -0
- package/dist/generation.js +84 -0
- package/dist/image-client.d.ts +6 -4
- package/dist/image-client.js +11 -11
- package/dist/image.d.ts +1040 -62
- package/dist/image.js +80 -52
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +9 -1
- package/dist/media.d.ts +212 -0
- package/dist/media.js +228 -0
- package/dist/promise.d.ts +554 -0
- package/dist/promise.js +44 -0
- package/dist/protocols/anthropic-messages.js +7 -17
- package/dist/protocols/bedrock-converse.d.ts +4 -4
- package/dist/protocols/bedrock-converse.js +1 -6
- package/dist/protocols/gemini.d.ts +21 -0
- package/dist/protocols/gemini.js +42 -6
- package/dist/protocols/google-images.d.ts +9 -21
- package/dist/protocols/google-images.js +169 -132
- package/dist/protocols/meta-images.d.ts +7 -12
- package/dist/protocols/meta-images.js +92 -66
- package/dist/protocols/mistral-chat.js +7 -6
- package/dist/protocols/open-responses.d.ts +11 -3
- package/dist/protocols/open-responses.js +23 -12
- package/dist/protocols/openai-chat.d.ts +34 -1
- package/dist/protocols/openai-chat.js +100 -31
- package/dist/protocols/openai-images.d.ts +9 -20
- package/dist/protocols/openai-images.js +112 -142
- package/dist/protocols/shared.d.ts +17 -17
- package/dist/protocols/shared.js +32 -35
- package/dist/protocols/utils/bedrock-media.d.ts +2 -3
- package/dist/protocols/utils/bedrock-media.js +4 -4
- package/dist/protocols/utils/media-input.d.ts +10 -0
- package/dist/protocols/utils/media-input.js +17 -0
- package/dist/protocols/utils/responses-compaction.js +6 -5
- package/dist/protocols/utils/tool-stream.d.ts +27 -3
- package/dist/protocols/xai-images.d.ts +9 -15
- package/dist/protocols/xai-images.js +85 -83
- package/dist/protocols/zai-images.d.ts +9 -13
- package/dist/protocols/zai-images.js +59 -57
- package/dist/providers/amazon-bedrock.d.ts +2 -2
- package/dist/providers/cerebras.js +6 -1
- package/dist/providers/deepinfra.js +6 -1
- package/dist/providers/google-vertex.d.ts +7 -0
- package/dist/providers/google.d.ts +7 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/openrouter.d.ts +19 -0
- package/dist/providers/openrouter.js +13 -1
- package/dist/providers/vercel-ai-gateway.d.ts +41 -0
- package/dist/providers/vercel-ai-gateway.js +85 -0
- package/dist/route/client.d.ts +9 -1
- package/dist/route/endpoint.d.ts +10 -10
- package/dist/route/executor-service.d.ts +12 -0
- package/dist/route/executor-service.js +3 -0
- package/dist/route/executor.d.ts +4 -9
- package/dist/route/executor.js +3 -3
- package/dist/route/index.d.ts +2 -0
- package/dist/route/index.js +2 -0
- package/dist/route/media-protocol.d.ts +45 -0
- package/dist/route/media-protocol.js +40 -0
- package/dist/route/media.d.ts +46 -0
- package/dist/route/media.js +64 -0
- package/dist/schema/errors.d.ts +13 -3
- package/dist/schema/errors.js +7 -0
- package/dist/schema/events.d.ts +563 -40
- package/dist/schema/events.js +35 -2
- package/dist/schema/messages.d.ts +98 -8
- package/dist/schema/messages.js +8 -6
- package/dist/schema/options.d.ts +2 -0
- package/dist/schema/options.js +3 -0
- package/dist/testing.d.ts +72 -8
- package/dist/utils/media-type.d.ts +6 -0
- package/dist/utils/media-type.js +49 -0
- package/dist/utils/sanitize.js +3 -1
- package/package.json +7 -3
- package/dist/protocols/utils/image-input.d.ts +0 -21
- package/dist/protocols/utils/image-input.js +0 -20
|
@@ -3,7 +3,8 @@ import { Route } from "../route/client.js";
|
|
|
3
3
|
import { Endpoint } from "../route/endpoint.js";
|
|
4
4
|
import { Protocol } from "../route/protocol.js";
|
|
5
5
|
import { AuthOptions } from "../route/auth-options.js";
|
|
6
|
-
import { ProviderID } from "../schema/index.js";
|
|
6
|
+
import { HttpOptions, ProviderID } from "../schema/index.js";
|
|
7
|
+
import { SystemOne } from "../experimental/system-one.js";
|
|
7
8
|
import { OpenAIChat } from "../protocols/openai-chat.js";
|
|
8
9
|
import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js";
|
|
9
10
|
import { isRecord } from "../protocols/shared.js";
|
|
@@ -98,13 +99,24 @@ const configuredRoute = (input) => {
|
|
|
98
99
|
};
|
|
99
100
|
export const configure = (input = {}) => {
|
|
100
101
|
const route = configuredRoute(input);
|
|
102
|
+
const evaluation = (modelID) => SystemOne.model({
|
|
103
|
+
id: modelID,
|
|
104
|
+
provider: id,
|
|
105
|
+
providerMetadataKey: "openrouter",
|
|
106
|
+
auth: AuthOptions.bearer(input, "OPENROUTER_API_KEY"),
|
|
107
|
+
baseURL: input.baseURL ?? baseURL,
|
|
108
|
+
headers: input.headers,
|
|
109
|
+
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
|
110
|
+
});
|
|
101
111
|
return {
|
|
102
112
|
id,
|
|
103
113
|
model: (modelID) => route.model({ id: modelID, compatibility: { supportsPromptCacheKey: true } }),
|
|
114
|
+
experimental: { evaluation },
|
|
104
115
|
configure,
|
|
105
116
|
};
|
|
106
117
|
};
|
|
107
118
|
export const provider = configure();
|
|
119
|
+
export const experimental = provider.experimental;
|
|
108
120
|
export const model = (modelID, { apiKey, baseURL, body, headers, ...providerOptions }) => configure({
|
|
109
121
|
apiKey,
|
|
110
122
|
baseURL,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EvaluationModel } from "../experimental/evaluation.js";
|
|
2
|
+
import { type ProviderAuthOption } from "../route/auth-options.js";
|
|
3
|
+
import { HttpOptions, ModelID } from "../schema/index.js";
|
|
4
|
+
export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
5
|
+
export interface EvaluationOptions {
|
|
6
|
+
readonly [key: string]: unknown;
|
|
7
|
+
readonly gateway?: Readonly<{
|
|
8
|
+
readonly [key: string]: unknown;
|
|
9
|
+
readonly zeroDataRetention?: boolean;
|
|
10
|
+
readonly only?: ReadonlyArray<string>;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export type Options = ProviderAuthOption<"optional"> & {
|
|
14
|
+
readonly baseURL?: string;
|
|
15
|
+
readonly headers?: Record<string, string>;
|
|
16
|
+
readonly http?: HttpOptions.Input;
|
|
17
|
+
};
|
|
18
|
+
export declare const configure: (input?: Options) => {
|
|
19
|
+
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
20
|
+
experimental: {
|
|
21
|
+
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
22
|
+
};
|
|
23
|
+
configure: (input?: Options) => /*elided*/ any;
|
|
24
|
+
};
|
|
25
|
+
export declare const provider: {
|
|
26
|
+
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
27
|
+
experimental: {
|
|
28
|
+
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
29
|
+
};
|
|
30
|
+
configure: (input?: Options) => {
|
|
31
|
+
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
32
|
+
experimental: {
|
|
33
|
+
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
34
|
+
};
|
|
35
|
+
configure: /*elided*/ any;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare const experimental: {
|
|
39
|
+
evaluation: (modelID: string | ModelID) => EvaluationModel<EvaluationOptions>;
|
|
40
|
+
};
|
|
41
|
+
export * as VercelAIGateway from "./vercel-ai-gateway.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import { EvaluationAnswer, EvaluationInput, EvaluationModel, EvaluationQuestion, EvaluationResponse, EvaluationRounding, } from "../experimental/evaluation.js";
|
|
4
|
+
import { Auth } from "../route/auth.js";
|
|
5
|
+
import { AuthOptions } from "../route/auth-options.js";
|
|
6
|
+
import { AIError, HttpContext, HttpOptions, InvalidProviderOutputError, InvalidRequestError, ModelID, ProviderID, ProviderMetadata, Usage, } from "../schema/index.js";
|
|
7
|
+
export const id = ProviderID.make("vercel-ai-gateway");
|
|
8
|
+
const baseURL = "https://ai-gateway.vercel.sh/v1";
|
|
9
|
+
const Request = Schema.StructWithRest(Schema.Struct({
|
|
10
|
+
model: Schema.String,
|
|
11
|
+
state: EvaluationInput,
|
|
12
|
+
questions: Schema.Record(Schema.String, EvaluationQuestion),
|
|
13
|
+
providerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
14
|
+
}), [Schema.Record(Schema.String, Schema.Any)]);
|
|
15
|
+
const Response = Schema.Struct({
|
|
16
|
+
model: Schema.optional(Schema.String),
|
|
17
|
+
answers: Schema.Record(Schema.String, EvaluationAnswer),
|
|
18
|
+
usage: Schema.optional(Schema.Struct({
|
|
19
|
+
inputTokens: Schema.optional(Schema.Number),
|
|
20
|
+
outputTokens: Schema.optional(Schema.Number),
|
|
21
|
+
})),
|
|
22
|
+
rounding: Schema.optional(EvaluationRounding),
|
|
23
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
24
|
+
});
|
|
25
|
+
export const configure = (input = {}) => {
|
|
26
|
+
const evaluation = (modelID) => EvaluationModel.make({
|
|
27
|
+
id: modelID,
|
|
28
|
+
provider: id,
|
|
29
|
+
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
|
30
|
+
route: {
|
|
31
|
+
id: "vercel-evaluation",
|
|
32
|
+
evaluate: (req, send) => Effect.gen(function* () {
|
|
33
|
+
const url = new URL(`${(input.baseURL ?? baseURL).replace(/\/$/, "")}/evaluate`);
|
|
34
|
+
Object.entries(req.http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value));
|
|
35
|
+
const body = yield* Schema.encodeUnknownEffect(Schema.fromJsonString(Request))({
|
|
36
|
+
...req.http?.body,
|
|
37
|
+
model: req.model.id,
|
|
38
|
+
state: req.state,
|
|
39
|
+
questions: req.questions,
|
|
40
|
+
providerOptions: req.options,
|
|
41
|
+
}).pipe(Effect.mapError((cause) => new AIError({ reason: new InvalidRequestError({ message: cause.message, cause }) })));
|
|
42
|
+
const headers = yield* Auth.toEffect(AuthOptions.bearer(input, ["AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN"]))({
|
|
43
|
+
request: req,
|
|
44
|
+
method: "POST",
|
|
45
|
+
url: url.toString(),
|
|
46
|
+
body,
|
|
47
|
+
headers: Headers.fromInput({ ...input.headers, ...req.http?.headers }),
|
|
48
|
+
});
|
|
49
|
+
const res = yield* send(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(body, "application/json")));
|
|
50
|
+
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers });
|
|
51
|
+
const fail = (message, cause, body) => new AIError({
|
|
52
|
+
reason: new InvalidProviderOutputError({
|
|
53
|
+
route: "vercel-evaluation",
|
|
54
|
+
message,
|
|
55
|
+
body,
|
|
56
|
+
http,
|
|
57
|
+
cause,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const text = yield* res.text.pipe(Effect.mapError((cause) => fail("Failed to read the Vercel AI Gateway evaluation response", cause)));
|
|
61
|
+
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(Effect.mapError((cause) => fail("Vercel AI Gateway returned an invalid evaluation response", cause, text)));
|
|
62
|
+
return new EvaluationResponse({
|
|
63
|
+
model: ModelID.make(data.model ?? req.model.id),
|
|
64
|
+
answers: data.answers,
|
|
65
|
+
usage: data.usage
|
|
66
|
+
? new Usage({
|
|
67
|
+
inputTokens: data.usage.inputTokens,
|
|
68
|
+
outputTokens: data.usage.outputTokens,
|
|
69
|
+
totalTokens: data.usage.inputTokens === undefined && data.usage.outputTokens === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: (data.usage.inputTokens ?? 0) + (data.usage.outputTokens ?? 0),
|
|
72
|
+
providerMetadata: { gateway: data.usage },
|
|
73
|
+
})
|
|
74
|
+
: undefined,
|
|
75
|
+
rounding: data.rounding,
|
|
76
|
+
providerMetadata: data.providerMetadata,
|
|
77
|
+
});
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return { id, experimental: { evaluation }, configure };
|
|
82
|
+
};
|
|
83
|
+
export const provider = configure();
|
|
84
|
+
export const experimental = provider.experimental;
|
|
85
|
+
export * as VercelAIGateway from "./vercel-ai-gateway.js";
|
package/dist/route/client.d.ts
CHANGED
|
@@ -381,6 +381,14 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
|
|
|
381
381
|
};
|
|
382
382
|
} | undefined;
|
|
383
383
|
readonly namespace?: string | undefined;
|
|
384
|
+
} | {
|
|
385
|
+
readonly type: "media";
|
|
386
|
+
readonly media: import("../media.js").Asset;
|
|
387
|
+
readonly providerMetadata?: {
|
|
388
|
+
readonly [x: string]: {
|
|
389
|
+
readonly [x: string]: unknown;
|
|
390
|
+
};
|
|
391
|
+
} | undefined;
|
|
384
392
|
} | {
|
|
385
393
|
readonly type: "step-finish";
|
|
386
394
|
readonly reason: {
|
|
@@ -409,12 +417,12 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
|
|
|
409
417
|
} | {
|
|
410
418
|
readonly type: "provider-error";
|
|
411
419
|
readonly message: string;
|
|
420
|
+
readonly classification?: "context-overflow" | "payload-too-large" | undefined;
|
|
412
421
|
readonly providerMetadata?: {
|
|
413
422
|
readonly [x: string]: {
|
|
414
423
|
readonly [x: string]: unknown;
|
|
415
424
|
};
|
|
416
425
|
} | undefined;
|
|
417
|
-
readonly classification?: "context-overflow" | "payload-too-large" | undefined;
|
|
418
426
|
}, AIError, Service>;
|
|
419
427
|
export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
|
|
420
428
|
export declare const Route: {
|
package/dist/route/endpoint.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { LLMRequest } from "../schema/index.js";
|
|
2
|
-
export interface EndpointInput<Body> {
|
|
3
|
-
readonly request:
|
|
2
|
+
export interface EndpointInput<Body, Request = LLMRequest> {
|
|
3
|
+
readonly request: Request;
|
|
4
4
|
readonly body: Body;
|
|
5
5
|
}
|
|
6
|
-
export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => string);
|
|
6
|
+
export type EndpointPart<Body, Request = LLMRequest> = string | ((input: EndpointInput<Body, Request>) => string);
|
|
7
7
|
/**
|
|
8
8
|
* Declarative URL construction for one route.
|
|
9
9
|
*
|
|
@@ -13,16 +13,16 @@ export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => strin
|
|
|
13
13
|
*
|
|
14
14
|
* `path` may be a string or a function of `EndpointInput`, for routes whose
|
|
15
15
|
* URL embeds the model id, region, or another body field (e.g. Bedrock,
|
|
16
|
-
* Gemini).
|
|
16
|
+
* Gemini). Media routes reuse the same shape with their own request type.
|
|
17
17
|
*/
|
|
18
|
-
export interface Definition<Body> {
|
|
18
|
+
export interface Definition<Body, Request = LLMRequest> {
|
|
19
19
|
readonly baseURL?: string;
|
|
20
|
-
readonly path: EndpointPart<Body>;
|
|
20
|
+
readonly path: EndpointPart<Body, Request>;
|
|
21
21
|
readonly query?: Record<string, string>;
|
|
22
22
|
}
|
|
23
|
-
export type EndpointPatch<Body> = Partial<Definition<Body>>;
|
|
23
|
+
export type EndpointPatch<Body, Request = LLMRequest> = Partial<Definition<Body, Request>>;
|
|
24
24
|
/** Construct an `Endpoint` from a path string or path function. */
|
|
25
|
-
export declare const path: <Body>(value: EndpointPart<Body>, options?: Omit<Definition<Body>, "path">) => Definition<Body>;
|
|
26
|
-
export declare const merge: <Body>(base: Definition<Body>, patch: EndpointPatch<Body>) => Definition<Body>;
|
|
27
|
-
export declare const render: <Body>(endpoint: Definition<Body>, input: EndpointInput<Body>) => URL;
|
|
25
|
+
export declare const path: <Body, Request = LLMRequest>(value: EndpointPart<Body, Request>, options?: Omit<Definition<Body, Request>, "path">) => Definition<Body, Request>;
|
|
26
|
+
export declare const merge: <Body, Request = LLMRequest>(base: Definition<Body, Request>, patch: EndpointPatch<Body, Request>) => Definition<Body, Request>;
|
|
27
|
+
export declare const render: <Body, Request = LLMRequest>(endpoint: Definition<Body, Request>, input: EndpointInput<Body, Request>) => URL;
|
|
28
28
|
export * as Endpoint from "./endpoint.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
import type { HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import type { AIError } from "../schema/errors.js";
|
|
4
|
+
export interface Interface {
|
|
5
|
+
readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
|
|
6
|
+
}
|
|
7
|
+
export type HttpHandler = (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
|
|
8
|
+
export type HttpMiddleware = (request: HttpClientRequest.HttpClientRequest, handler: HttpHandler) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
|
|
9
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/AI/RequestExecutor", Interface>;
|
|
10
|
+
export declare class Service extends Service_base {
|
|
11
|
+
}
|
|
12
|
+
export {};
|
package/dist/route/executor.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Layer, Stream } from "effect";
|
|
2
2
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
3
3
|
import { HttpContext, AIError } from "../schema/index.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export type HttpHandler = (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
|
|
8
|
-
export type HttpMiddleware = (request: HttpClientRequest.HttpClientRequest, handler: HttpHandler) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
|
|
9
|
-
declare const Service_base: Context.ServiceClass<Service, "@opencode/AI/RequestExecutor", Interface>;
|
|
10
|
-
export declare class Service extends Service_base {
|
|
11
|
-
}
|
|
4
|
+
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js";
|
|
5
|
+
export { Service } from "./executor-service.js";
|
|
6
|
+
export type { HttpHandler, HttpMiddleware, Interface } from "./executor-service.js";
|
|
12
7
|
export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext;
|
|
13
8
|
/** Preserve HTTP diagnostics for executor and externally captured failures alike. */
|
|
14
9
|
export declare const httpFailure: (input: {
|
package/dist/route/executor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Cause,
|
|
1
|
+
import { Cause, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
|
|
3
3
|
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js";
|
|
4
4
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
5
|
-
|
|
6
|
-
}
|
|
5
|
+
import { Service } from "./executor-service.js";
|
|
6
|
+
export { Service } from "./executor-service.js";
|
|
7
7
|
const headerDetails = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]));
|
|
8
8
|
const normalizedHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
9
9
|
const retryAfterMs = (headers) => {
|
package/dist/route/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from "./executor.js";
|
|
|
4
4
|
export { Auth } from "./auth.js";
|
|
5
5
|
export { AuthOptions } from "./auth-options.js";
|
|
6
6
|
export { Endpoint } from "./endpoint.js";
|
|
7
|
+
export { MediaRoute } from "./media.js";
|
|
8
|
+
export { MediaProtocol } from "./media-protocol.js";
|
|
7
9
|
export { Framing } from "./framing.js";
|
|
8
10
|
export { Protocol } from "./protocol.js";
|
|
9
11
|
export { HttpTransport, WebSocketTransport } from "./transport/index.js";
|
package/dist/route/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./executor.js";
|
|
|
3
3
|
export { Auth } from "./auth.js";
|
|
4
4
|
export { AuthOptions } from "./auth-options.js";
|
|
5
5
|
export { Endpoint } from "./endpoint.js";
|
|
6
|
+
export { MediaRoute } from "./media.js";
|
|
7
|
+
export { MediaProtocol } from "./media-protocol.js";
|
|
6
8
|
export { Framing } from "./framing.js";
|
|
7
9
|
export { Protocol } from "./protocol.js";
|
|
8
10
|
export { HttpTransport, WebSocketTransport } from "./transport/index.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import { AIError, HttpContext } from "../schema/index.js";
|
|
4
|
+
export type Body = {
|
|
5
|
+
readonly type: "json";
|
|
6
|
+
readonly value: Record<string, unknown>;
|
|
7
|
+
} | {
|
|
8
|
+
readonly type: "multipart";
|
|
9
|
+
readonly value: FormData;
|
|
10
|
+
};
|
|
11
|
+
export declare const json: (value: Record<string, unknown>) => Body;
|
|
12
|
+
export declare const multipart: (value: FormData) => Body;
|
|
13
|
+
export interface DecodeContext<Request> {
|
|
14
|
+
readonly request: Request;
|
|
15
|
+
readonly body: Body;
|
|
16
|
+
}
|
|
17
|
+
/** One request, one response. JSON or multipart in; JSON or raw bytes out. */
|
|
18
|
+
export interface Inline<Request, Response> {
|
|
19
|
+
readonly kind: "inline";
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Common request fields this protocol cannot lower; the route rejects them before `body.from` runs. */
|
|
23
|
+
readonly unsupported?: ReadonlyArray<keyof Request & string>;
|
|
24
|
+
readonly body: {
|
|
25
|
+
readonly from: (request: Request) => Effect.Effect<Body, AIError>;
|
|
26
|
+
};
|
|
27
|
+
readonly response: {
|
|
28
|
+
readonly decode: (response: HttpClientResponse.HttpClientResponse, context: DecodeContext<Request>) => Effect.Effect<Response, AIError>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare const inline: <Request, Response>(input: Omit<Inline<Request, Response>, "kind">) => Inline<Request, Response>;
|
|
32
|
+
/** Read a text body while retaining the original payload and HTTP context on every downstream error. */
|
|
33
|
+
export declare const text: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
|
|
34
|
+
body: string;
|
|
35
|
+
http: HttpContext;
|
|
36
|
+
invalid: (message: string, cause?: unknown) => AIError;
|
|
37
|
+
}, AIError, never>;
|
|
38
|
+
/** Read and Schema-decode a JSON body. Decode failures keep the raw body as `reason.body`. */
|
|
39
|
+
export declare const decodeJson: <A>(route: string, name: string, schema: Schema.Codec<A, unknown>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
|
|
40
|
+
value: A;
|
|
41
|
+
body: string;
|
|
42
|
+
http: HttpContext;
|
|
43
|
+
invalid: (message: string, cause?: unknown) => AIError;
|
|
44
|
+
}, AIError, never>;
|
|
45
|
+
export * as MediaProtocol from "./media-protocol.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import { AIError, HttpContext, InvalidProviderOutputError } from "../schema/index.js";
|
|
4
|
+
export const json = (value) => ({ type: "json", value });
|
|
5
|
+
export const multipart = (value) => ({ type: "multipart", value });
|
|
6
|
+
export const inline = (input) => ({
|
|
7
|
+
kind: "inline",
|
|
8
|
+
...input,
|
|
9
|
+
});
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Response helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const context = (response) => new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
|
|
14
|
+
/** Read a text body while retaining the original payload and HTTP context on every downstream error. */
|
|
15
|
+
export const text = Effect.fn("MediaProtocol.text")(function* (route, name, response) {
|
|
16
|
+
const http = context(response);
|
|
17
|
+
const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
|
|
18
|
+
reason: new InvalidProviderOutputError({
|
|
19
|
+
route,
|
|
20
|
+
message: `Failed to read the ${name} response`,
|
|
21
|
+
http,
|
|
22
|
+
cause,
|
|
23
|
+
}),
|
|
24
|
+
})));
|
|
25
|
+
return {
|
|
26
|
+
body,
|
|
27
|
+
http,
|
|
28
|
+
invalid: (message, cause) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body, http, cause }) }),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
/** Read and Schema-decode a JSON body. Decode failures keep the raw body as `reason.body`. */
|
|
32
|
+
export const decodeJson = (route, name, schema) => {
|
|
33
|
+
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
|
|
34
|
+
return Effect.fn("MediaProtocol.decodeJson")(function* (response) {
|
|
35
|
+
const output = yield* text(route, name, response);
|
|
36
|
+
const value = yield* decode(output.body).pipe(Effect.mapError((cause) => output.invalid(`${name} returned an invalid response`, cause)));
|
|
37
|
+
return { ...output, value };
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
export * as MediaProtocol from "./media-protocol.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Auth } from "./auth.js";
|
|
3
|
+
import { Endpoint } from "./endpoint.js";
|
|
4
|
+
import type { Interface } from "./executor-service.js";
|
|
5
|
+
import { MediaProtocol } from "./media-protocol.js";
|
|
6
|
+
import { AIError, HttpOptions, ProviderID } from "../schema/index.js";
|
|
7
|
+
export type Execute = Interface["execute"];
|
|
8
|
+
/** The minimum a media request must carry for the route to build a transport request. */
|
|
9
|
+
export interface MediaRequest {
|
|
10
|
+
readonly model: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly provider: ProviderID;
|
|
13
|
+
readonly http?: HttpOptions;
|
|
14
|
+
};
|
|
15
|
+
readonly http?: HttpOptions;
|
|
16
|
+
}
|
|
17
|
+
/** Deployment inputs every media model factory accepts; provider facades fill these from `configure(...)`. */
|
|
18
|
+
export interface ModelInput {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly auth: Auth.Definition;
|
|
21
|
+
readonly baseURL?: string;
|
|
22
|
+
readonly headers?: Record<string, string>;
|
|
23
|
+
readonly http?: HttpOptions;
|
|
24
|
+
}
|
|
25
|
+
export interface Route<Request extends MediaRequest, Response> {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly provider: ProviderID;
|
|
28
|
+
readonly protocol: string;
|
|
29
|
+
readonly generate: (request: Request, execute: Execute) => Effect.Effect<Response, AIError>;
|
|
30
|
+
}
|
|
31
|
+
export interface MakeInput<Request extends MediaRequest, Response> {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly provider: string | ProviderID;
|
|
34
|
+
readonly protocol: MediaProtocol.Inline<Request, Response>;
|
|
35
|
+
readonly endpoint: Endpoint.Definition<MediaProtocol.Body, Request>;
|
|
36
|
+
readonly auth: Auth.Definition;
|
|
37
|
+
/** Deployment headers applied before transport authentication. */
|
|
38
|
+
readonly headers?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
|
|
42
|
+
* plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field rejection, URL and query
|
|
43
|
+
* rendering, auth headers, JSON vs multipart encoding, and handing the response back to the protocol for decoding.
|
|
44
|
+
*/
|
|
45
|
+
export declare const make: <Request extends MediaRequest, Response>(input: MakeInput<Request, Response>) => Route<Request, Response>;
|
|
46
|
+
export * as MediaRoute from "./media.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import { Auth } from "./auth.js";
|
|
4
|
+
import { Endpoint } from "./endpoint.js";
|
|
5
|
+
import { MediaProtocol } from "./media-protocol.js";
|
|
6
|
+
import { ProviderShared } from "../protocols/shared.js";
|
|
7
|
+
import { AIError, HttpOptions, ProviderID, mergeHttpOptions } from "../schema/index.js";
|
|
8
|
+
import { sanitizeSurrogates } from "../utils/sanitize.js";
|
|
9
|
+
/**
|
|
10
|
+
* Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
|
|
11
|
+
* plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field rejection, URL and query
|
|
12
|
+
* rendering, auth headers, JSON vs multipart encoding, and handing the response back to the protocol for decoding.
|
|
13
|
+
*/
|
|
14
|
+
export const make = (input) => {
|
|
15
|
+
const provider = ProviderID.make(input.provider);
|
|
16
|
+
const routeHttp = input.headers === undefined ? undefined : new HttpOptions({ headers: input.headers });
|
|
17
|
+
const authorize = Auth.toEffect(input.auth);
|
|
18
|
+
return {
|
|
19
|
+
id: input.id,
|
|
20
|
+
provider,
|
|
21
|
+
protocol: input.protocol.id,
|
|
22
|
+
generate: Effect.fn(`MediaRoute.generate`)(function* (request, execute) {
|
|
23
|
+
yield* rejectUnsupported(input.id, provider, request, input.protocol.unsupported);
|
|
24
|
+
const http = mergeHttpOptions(routeHttp, request.model.http, request.http);
|
|
25
|
+
// Sanitize after merging so model-level overlays are covered; the model value is restored, not sanitized.
|
|
26
|
+
const resolved = { ...sanitizeSurrogates({ ...request, http }), model: request.model };
|
|
27
|
+
const body = yield* input.protocol.body.from(resolved);
|
|
28
|
+
const url = Endpoint.render(input.endpoint, { request: resolved, body });
|
|
29
|
+
for (const [key, value] of Object.entries(http?.query ?? {}))
|
|
30
|
+
url.searchParams.set(key, value);
|
|
31
|
+
const encoded = body.type === "json" ? ProviderShared.encodeJson(body.value) : "[multipart/form-data]";
|
|
32
|
+
const baseHeaders = Headers.fromInput(http?.headers);
|
|
33
|
+
const headers = yield* authorize({
|
|
34
|
+
request: resolved,
|
|
35
|
+
method: "POST",
|
|
36
|
+
url: url.toString(),
|
|
37
|
+
body: encoded,
|
|
38
|
+
// The HTTP client sets the multipart boundary; a caller-supplied content-type would corrupt it.
|
|
39
|
+
headers: body.type === "multipart" ? Headers.remove(baseHeaders, "content-type") : baseHeaders,
|
|
40
|
+
});
|
|
41
|
+
const transport = HttpClientRequest.post(url.toString()).pipe(HttpClientRequest.setHeaders(headers), body.type === "json"
|
|
42
|
+
? HttpClientRequest.bodyText(encoded, "application/json")
|
|
43
|
+
: HttpClientRequest.bodyFormData(body.value));
|
|
44
|
+
const response = yield* execute(transport);
|
|
45
|
+
return yield* input.protocol.response.decode(response, { request: resolved, body });
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Common fields are never silently dropped: a present field the protocol declared unsupported fails typed. */
|
|
50
|
+
const rejectUnsupported = (route, provider, request, unsupported) => {
|
|
51
|
+
const present = (unsupported ?? []).filter((field) => {
|
|
52
|
+
const value = request[field];
|
|
53
|
+
return Array.isArray(value) ? value.length > 0 : value !== undefined;
|
|
54
|
+
});
|
|
55
|
+
if (present.length === 0)
|
|
56
|
+
return Effect.void;
|
|
57
|
+
return Effect.fail(ProviderShared.unsupportedOperation({
|
|
58
|
+
operation: `media.${present[0]}`,
|
|
59
|
+
provider,
|
|
60
|
+
route,
|
|
61
|
+
message: `${provider}/${route} does not support ${present.join(", ")}`,
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
export * as MediaRoute from "./media.js";
|
package/dist/schema/errors.d.ts
CHANGED
|
@@ -147,13 +147,23 @@ declare const UnknownProviderError_base: Schema.Class<UnknownProviderError, Sche
|
|
|
147
147
|
}>, import("effect/Cause").YieldableError>;
|
|
148
148
|
export declare class UnknownProviderError extends UnknownProviderError_base {
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
declare const TimeoutError_base: Schema.Class<TimeoutError, Schema.TaggedStruct<"Timeout", {
|
|
151
|
+
readonly timeoutMs: Schema.optional<Schema.Number>;
|
|
152
|
+
readonly message: Schema.String;
|
|
153
|
+
readonly body: Schema.optional<Schema.String>;
|
|
154
|
+
readonly http: Schema.optional<typeof HttpContext>;
|
|
155
|
+
readonly cause: Schema.optional<Schema.Defect>;
|
|
156
|
+
}>, import("effect/Cause").YieldableError>;
|
|
157
|
+
/** A caller-supplied deadline elapsed, such as `Generation.await` polling past its `Poll.timeout`. */
|
|
158
|
+
export declare class TimeoutError extends TimeoutError_base {
|
|
159
|
+
}
|
|
160
|
+
export declare const AIErrorReason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestError, typeof UnsupportedOperationError, typeof NoRouteError, typeof AuthenticationError, typeof RateLimitError, typeof QuotaExceededError, typeof ContentPolicyError, typeof ProviderInternalError, typeof TransportError, typeof InvalidProviderOutputError, typeof UnknownProviderError, typeof TimeoutError]>;
|
|
151
161
|
export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>;
|
|
152
162
|
declare const AIError_base: Schema.Class<AIError, Schema.TaggedStruct<"AI.Error", {
|
|
153
|
-
readonly reason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestError, typeof UnsupportedOperationError, typeof NoRouteError, typeof AuthenticationError, typeof RateLimitError, typeof QuotaExceededError, typeof ContentPolicyError, typeof ProviderInternalError, typeof TransportError, typeof InvalidProviderOutputError, typeof UnknownProviderError]>;
|
|
163
|
+
readonly reason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestError, typeof UnsupportedOperationError, typeof NoRouteError, typeof AuthenticationError, typeof RateLimitError, typeof QuotaExceededError, typeof ContentPolicyError, typeof ProviderInternalError, typeof TransportError, typeof InvalidProviderOutputError, typeof UnknownProviderError, typeof TimeoutError]>;
|
|
154
164
|
}>, import("effect/Cause").YieldableError>;
|
|
155
165
|
export declare class AIError extends AIError_base {
|
|
156
|
-
readonly cause: InvalidRequestError | UnsupportedOperationError | NoRouteError | AuthenticationError | RateLimitError | QuotaExceededError | ContentPolicyError | ProviderInternalError | TransportError | InvalidProviderOutputError | UnknownProviderError;
|
|
166
|
+
readonly cause: InvalidRequestError | UnsupportedOperationError | NoRouteError | AuthenticationError | RateLimitError | QuotaExceededError | ContentPolicyError | ProviderInternalError | TransportError | InvalidProviderOutputError | UnknownProviderError | TimeoutError;
|
|
157
167
|
get message(): string;
|
|
158
168
|
}
|
|
159
169
|
/**
|
package/dist/schema/errors.js
CHANGED
|
@@ -97,6 +97,12 @@ export class InvalidProviderOutputError extends Schema.TaggedError("AI.Error.Inv
|
|
|
97
97
|
}
|
|
98
98
|
export class UnknownProviderError extends Schema.TaggedError("AI.Error.UnknownProvider")("UnknownProvider", ReasonFields) {
|
|
99
99
|
}
|
|
100
|
+
/** A caller-supplied deadline elapsed, such as `Generation.await` polling past its `Poll.timeout`. */
|
|
101
|
+
export class TimeoutError extends Schema.TaggedError("AI.Error.Timeout")("Timeout", {
|
|
102
|
+
...ReasonFields,
|
|
103
|
+
timeoutMs: Schema.optional(Schema.Number),
|
|
104
|
+
}) {
|
|
105
|
+
}
|
|
100
106
|
export const AIErrorReason = Schema.Union([
|
|
101
107
|
InvalidRequestError,
|
|
102
108
|
UnsupportedOperationError,
|
|
@@ -109,6 +115,7 @@ export const AIErrorReason = Schema.Union([
|
|
|
109
115
|
TransportError,
|
|
110
116
|
InvalidProviderOutputError,
|
|
111
117
|
UnknownProviderError,
|
|
118
|
+
TimeoutError,
|
|
112
119
|
]).pipe(Schema.toTaggedUnion("_tag"));
|
|
113
120
|
export class AIError extends Schema.TaggedError()("AI.Error", {
|
|
114
121
|
reason: AIErrorReason,
|