@opencode/ai 0.0.0-dev-20039 → 0.0.0-dev-20041
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 +86 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/media-model.d.ts +3 -3
- package/dist/media-model.js +3 -3
- package/dist/promise.d.ts +111 -3
- package/dist/promise.js +9 -1
- package/dist/protocols/bedrock-converse.js +2 -8
- package/dist/protocols/cartesia-speech.d.ts +127 -0
- package/dist/protocols/cartesia-speech.js +126 -0
- package/dist/protocols/deepgram-speech.d.ts +119 -0
- package/dist/protocols/deepgram-speech.js +90 -0
- package/dist/protocols/elevenlabs-speech.d.ts +122 -0
- package/dist/protocols/elevenlabs-speech.js +115 -0
- package/dist/protocols/google-speech.d.ts +160 -0
- package/dist/protocols/google-speech.js +122 -0
- package/dist/protocols/openai-speech.d.ts +116 -0
- package/dist/protocols/openai-speech.js +98 -0
- package/dist/protocols/utils/speech-stream.d.ts +49 -0
- package/dist/protocols/utils/speech-stream.js +67 -0
- package/dist/providers/cartesia.d.ts +24 -0
- package/dist/providers/cartesia.js +22 -0
- package/dist/providers/deepgram.d.ts +24 -0
- package/dist/providers/deepgram.js +28 -0
- package/dist/providers/elevenlabs.d.ts +24 -0
- package/dist/providers/elevenlabs.js +28 -0
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.js +3 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/openai.d.ts +5 -0
- package/dist/providers/openai.js +6 -1
- package/dist/route/framing.d.ts +5 -1
- package/dist/route/framing.js +9 -0
- package/dist/route/media-protocol.d.ts +35 -2
- package/dist/route/media-protocol.js +22 -1
- package/dist/route/media.d.ts +15 -1
- package/dist/route/media.js +41 -7
- package/dist/speech-client.d.ts +21 -0
- package/dist/speech-client.js +25 -0
- package/dist/speech.d.ts +1150 -0
- package/dist/speech.js +119 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +10 -0
- package/dist/utils/media-type.js +8 -1
- package/package.json +3 -3
package/dist/providers/google.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Auth } from "../route/auth.js";
|
|
|
2
2
|
import { HttpOptions, ProviderID, mergeHttpOptions } from "../schema/index.js";
|
|
3
3
|
import { Gemini } from "../protocols/gemini.js";
|
|
4
4
|
import { GoogleImages } from "../protocols/google-images.js";
|
|
5
|
+
import { GoogleSpeech } from "../protocols/google-speech.js";
|
|
5
6
|
import { GoogleVideo } from "../protocols/google-video.js";
|
|
6
7
|
export const id = ProviderID.make("google");
|
|
7
8
|
export const routes = [Gemini.route];
|
|
@@ -30,6 +31,7 @@ export const configure = (input = {}) => {
|
|
|
30
31
|
model: (modelID) => route.model({ id: modelID }),
|
|
31
32
|
image: (modelID) => GoogleImages.model(media(modelID)),
|
|
32
33
|
video: (modelID) => GoogleVideo.model(media(modelID)),
|
|
34
|
+
speech: (modelID) => GoogleSpeech.model(media(modelID)),
|
|
33
35
|
configure,
|
|
34
36
|
};
|
|
35
37
|
};
|
|
@@ -43,3 +45,4 @@ export const model = (modelID, { apiKey, baseURL, body, headers, ...providerOpti
|
|
|
43
45
|
}).model(modelID);
|
|
44
46
|
export const image = provider.image;
|
|
45
47
|
export const video = provider.video;
|
|
48
|
+
export const speech = provider.speech;
|
|
@@ -5,11 +5,14 @@ export * as AmazonBedrock from "./amazon-bedrock.js";
|
|
|
5
5
|
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js";
|
|
6
6
|
export * as Azure from "./azure.js";
|
|
7
7
|
export * as Baseten from "./baseten.js";
|
|
8
|
+
export * as Cartesia from "./cartesia.js";
|
|
8
9
|
export * as Cerebras from "./cerebras.js";
|
|
9
10
|
export * as CloudflareAIGateway from "./cloudflare-ai-gateway.js";
|
|
10
11
|
export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js";
|
|
11
12
|
export * as DeepInfra from "./deepinfra.js";
|
|
13
|
+
export * as Deepgram from "./deepgram.js";
|
|
12
14
|
export * as DeepSeek from "./deepseek.js";
|
|
15
|
+
export * as ElevenLabs from "./elevenlabs.js";
|
|
13
16
|
export * as Fal from "./fal.js";
|
|
14
17
|
export * as Fireworks from "./fireworks.js";
|
|
15
18
|
export * as Google from "./google.js";
|
package/dist/providers/index.js
CHANGED
|
@@ -5,11 +5,14 @@ export * as AmazonBedrock from "./amazon-bedrock.js";
|
|
|
5
5
|
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js";
|
|
6
6
|
export * as Azure from "./azure.js";
|
|
7
7
|
export * as Baseten from "./baseten.js";
|
|
8
|
+
export * as Cartesia from "./cartesia.js";
|
|
8
9
|
export * as Cerebras from "./cerebras.js";
|
|
9
10
|
export * as CloudflareAIGateway from "./cloudflare-ai-gateway.js";
|
|
10
11
|
export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js";
|
|
11
12
|
export * as DeepInfra from "./deepinfra.js";
|
|
13
|
+
export * as Deepgram from "./deepgram.js";
|
|
12
14
|
export * as DeepSeek from "./deepseek.js";
|
|
15
|
+
export * as ElevenLabs from "./elevenlabs.js";
|
|
13
16
|
export * as Fal from "./fal.js";
|
|
14
17
|
export * as Fireworks from "./fireworks.js";
|
|
15
18
|
export * as Google from "./google.js";
|
|
@@ -7,6 +7,7 @@ import { type OpenAIProviderOptionsInput } from "./openai-options.js";
|
|
|
7
7
|
import { type OpenAIImageString } from "../protocols/openai-images.js";
|
|
8
8
|
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options.js";
|
|
9
9
|
export type { OpenAIImageOptions } from "../protocols/openai-images.js";
|
|
10
|
+
export type { OpenAISpeechOptions } from "../protocols/openai-speech.js";
|
|
10
11
|
export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
11
12
|
export declare const routes: (Route<{
|
|
12
13
|
readonly model: string;
|
|
@@ -372,6 +373,7 @@ export declare const configure: (input?: Config) => {
|
|
|
372
373
|
}>;
|
|
373
374
|
chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
|
|
374
375
|
image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
|
|
376
|
+
speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
|
|
375
377
|
configure: (input?: Config) => /*elided*/ any;
|
|
376
378
|
};
|
|
377
379
|
export declare const provider: {
|
|
@@ -386,6 +388,7 @@ export declare const provider: {
|
|
|
386
388
|
}>;
|
|
387
389
|
chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
|
|
388
390
|
image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
|
|
391
|
+
speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
|
|
389
392
|
configure: (input?: Config) => {
|
|
390
393
|
id: string & import("effect/Brand").Brand<"AI.ProviderID">;
|
|
391
394
|
model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, {
|
|
@@ -398,6 +401,7 @@ export declare const provider: {
|
|
|
398
401
|
}>;
|
|
399
402
|
chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
|
|
400
403
|
image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
|
|
404
|
+
speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
|
|
401
405
|
configure: /*elided*/ any;
|
|
402
406
|
};
|
|
403
407
|
};
|
|
@@ -409,3 +413,4 @@ export declare const responses: (id: string | ModelID) => import("../schema/opti
|
|
|
409
413
|
}>;
|
|
410
414
|
export declare const chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
|
|
411
415
|
export declare const image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
|
|
416
|
+
export declare const speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
|
package/dist/providers/openai.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as OpenAIChat from "../protocols/openai-chat.js";
|
|
|
4
4
|
import * as OpenAIResponses from "../protocols/openai-responses.js";
|
|
5
5
|
import { withOpenAIOptions } from "./openai-options.js";
|
|
6
6
|
import { OpenAIImages } from "../protocols/openai-images.js";
|
|
7
|
+
import { OpenAISpeech } from "../protocols/openai-speech.js";
|
|
7
8
|
export const id = ProviderID.make("openai");
|
|
8
9
|
export const routes = [OpenAIResponses.route, OpenAIChat.route];
|
|
9
10
|
export const imageGeneration = (options = {}) => ToolDefinition.make({
|
|
@@ -44,19 +45,22 @@ export const configure = (input = {}) => {
|
|
|
44
45
|
id,
|
|
45
46
|
compatibility: { supportsPromptCacheKey: true },
|
|
46
47
|
});
|
|
47
|
-
const
|
|
48
|
+
const media = (modelID) => ({
|
|
48
49
|
id: modelID,
|
|
49
50
|
auth: auth(input),
|
|
50
51
|
baseURL: input.baseURL,
|
|
51
52
|
headers: input.headers,
|
|
52
53
|
http: mergeHttpOptions(input.http === undefined ? undefined : HttpOptions.make(input.http), input.queryParams === undefined ? undefined : new HttpOptions({ query: input.queryParams })),
|
|
53
54
|
});
|
|
55
|
+
const image = (modelID) => OpenAIImages.model(media(modelID));
|
|
56
|
+
const speech = (modelID) => OpenAISpeech.model(media(modelID));
|
|
54
57
|
return {
|
|
55
58
|
id,
|
|
56
59
|
model: responses,
|
|
57
60
|
responses,
|
|
58
61
|
chat,
|
|
59
62
|
image,
|
|
63
|
+
speech,
|
|
60
64
|
configure,
|
|
61
65
|
};
|
|
62
66
|
};
|
|
@@ -83,3 +87,4 @@ export const chatModel = (modelID, settings) => configure(config(settings)).chat
|
|
|
83
87
|
export const responses = provider.responses;
|
|
84
88
|
export const chat = provider.chat;
|
|
85
89
|
export const image = provider.image;
|
|
90
|
+
export const speech = provider.speech;
|
package/dist/route/framing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Stream } from "effect";
|
|
2
2
|
import type { AIError } from "../schema/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Decode a streaming HTTP response body into provider-protocol frames.
|
|
@@ -10,6 +10,8 @@ import type { AIError } from "../schema/index.js";
|
|
|
10
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
|
+
* - Media streams — newline-delimited JSON (`lines`) or the whole body as one
|
|
14
|
+
* frame (`document`); chunked binary bodies need no framing.
|
|
13
15
|
*
|
|
14
16
|
* The frame type is opaque to this layer; the protocol's event schema decodes
|
|
15
17
|
* each frame before its state machine handles it.
|
|
@@ -26,4 +28,6 @@ export declare const sse: Definition<string>;
|
|
|
26
28
|
export declare const sseWithDone: Definition<string>;
|
|
27
29
|
/** SSE framing restricted to protocol-recognized event names. */
|
|
28
30
|
export declare const sseEvents: (events: ReadonlySet<string>) => Definition<string>;
|
|
31
|
+
export declare const lines: Definition<string>;
|
|
32
|
+
export declare const document: Definition<string>;
|
|
29
33
|
export * as Framing from "./framing.js";
|
package/dist/route/framing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Stream } from "effect";
|
|
1
2
|
import * as ProviderShared from "../protocols/shared.js";
|
|
2
3
|
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
|
3
4
|
export const sse = { id: "sse", frame: ProviderShared.sseFraming };
|
|
@@ -11,4 +12,12 @@ export const sseEvents = (events) => ({
|
|
|
11
12
|
id: "sse",
|
|
12
13
|
frame: (bytes) => ProviderShared.sseFraming(bytes, events),
|
|
13
14
|
});
|
|
15
|
+
export const lines = {
|
|
16
|
+
id: "lines",
|
|
17
|
+
frame: (bytes) => bytes.pipe(Stream.decodeText(), Stream.splitLines, Stream.filter((line) => line.trim().length > 0)),
|
|
18
|
+
};
|
|
19
|
+
export const document = {
|
|
20
|
+
id: "document",
|
|
21
|
+
frame: (bytes) => Stream.fromEffect(Stream.mkString(bytes.pipe(Stream.decodeText()))),
|
|
22
|
+
};
|
|
14
23
|
export * as Framing from "./framing.js";
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { Duration, Effect, Schema } from "effect";
|
|
1
|
+
import { Duration, Effect, Schema, type Stream } from "effect";
|
|
2
2
|
import { HttpClientResponse } from "effect/unstable/http";
|
|
3
3
|
import type { Snapshot, Status } from "../generation.js";
|
|
4
4
|
import { Media } from "../media.js";
|
|
5
5
|
import type { AuthInput } from "./auth.js";
|
|
6
6
|
import { AIError, HttpContext } from "../schema/index.js";
|
|
7
|
+
/** JSON `query` is appended to the endpoint URL before the route and caller `http.query` overlays. */
|
|
7
8
|
export type Body = {
|
|
8
9
|
readonly type: "json";
|
|
9
10
|
readonly value: Record<string, unknown>;
|
|
11
|
+
readonly query?: Record<string, string>;
|
|
10
12
|
} | {
|
|
11
13
|
readonly type: "multipart";
|
|
12
14
|
readonly value: FormData;
|
|
13
15
|
};
|
|
14
|
-
export declare const json: (value: Record<string, unknown>) => Body;
|
|
16
|
+
export declare const json: (value: Record<string, unknown>, query?: Record<string, string>) => Body;
|
|
15
17
|
export declare const multipart: (value: FormData) => Body;
|
|
16
18
|
export interface DecodeContext<Request> {
|
|
17
19
|
readonly request: Request;
|
|
@@ -80,6 +82,33 @@ export interface Queued<Request, Response, Token> {
|
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
export declare const queued: <Request, Response, Token>(input: Omit<Queued<Request, Response, Token>, "kind">) => Queued<Request, Response, Token>;
|
|
85
|
+
export type Mode = "generate" | "stream";
|
|
86
|
+
export type Addressed<Request> = Request & {
|
|
87
|
+
readonly mode: Mode;
|
|
88
|
+
};
|
|
89
|
+
export interface ResponseContext<Request> extends DecodeContext<Addressed<Request>> {
|
|
90
|
+
readonly http: HttpContext;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One request whose body is parsed incrementally, like LLM protocols: `frames` → `step`* → `finish`. `generate` and
|
|
94
|
+
* `stream` share this state machine; `request.mode` lets a protocol pick a different body, path, or framing.
|
|
95
|
+
*/
|
|
96
|
+
export interface Streamed<Request, Event, Frame, State> {
|
|
97
|
+
readonly kind: "stream";
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly name: string;
|
|
100
|
+
/** Common request fields this protocol cannot lower; the route rejects them before `body.from` runs. */
|
|
101
|
+
readonly unsupported?: ReadonlyArray<keyof Request & string>;
|
|
102
|
+
readonly body: {
|
|
103
|
+
readonly from: (request: Addressed<Request>) => Effect.Effect<Body, AIError>;
|
|
104
|
+
};
|
|
105
|
+
readonly frames: (bytes: Stream.Stream<Uint8Array, AIError>, context: DecodeContext<Addressed<Request>>) => Stream.Stream<Frame, AIError>;
|
|
106
|
+
readonly initial: () => State;
|
|
107
|
+
readonly step: (state: State, frame: Frame) => Effect.Effect<readonly [State, ReadonlyArray<Event>], AIError>;
|
|
108
|
+
/** Emit exactly one terminal event, or fail when the provider stopped before completing. */
|
|
109
|
+
readonly finish: (state: State, context: ResponseContext<Request>) => Effect.Effect<ReadonlyArray<Event>, AIError>;
|
|
110
|
+
}
|
|
111
|
+
export declare const stream: <Request, Event, Frame, State>(input: Omit<Streamed<Request, Event, Frame, State>, "kind">) => Streamed<Request, Event, Frame, State>;
|
|
83
112
|
/**
|
|
84
113
|
* Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a
|
|
85
114
|
* malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is
|
|
@@ -106,6 +135,10 @@ export declare const decodeJson: <A>(route: string, name: string, schema: Schema
|
|
|
106
135
|
export declare const decodeStarted: <A, Token>(route: string, name: string, schema: Schema.Codec<A, unknown>, started: (value: A) => Started<Token>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<Started<Token>, AIError, never>;
|
|
107
136
|
/** Map a provider status string through the protocol's table; unknown values are an invalid provider document. */
|
|
108
137
|
export declare const status: <Table extends Record<string, Status>>(table: Table, raw: string, output: Output) => Effect.Effect<Status, AIError>;
|
|
138
|
+
export declare const frameError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
|
|
139
|
+
export declare const incomplete: (route: string) => AIError;
|
|
140
|
+
/** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
|
|
141
|
+
export declare const decodeFrame: <A>(route: string, name: string, schema: Schema.Codec<A, unknown>) => (frame: string) => Effect.Effect<A, AIError, never>;
|
|
109
142
|
/** A `url` asset whose provider-declared retention window starts now. */
|
|
110
143
|
export declare const expiringUrl: (url: string, retention: Duration.Duration, options?: Parameters<typeof Media.url>[1]) => Effect.Effect<Media.Asset, never, never>;
|
|
111
144
|
export * as MediaProtocol from "./media-protocol.js";
|
|
@@ -2,7 +2,11 @@ import { Clock, Duration, Effect, Schema } from "effect";
|
|
|
2
2
|
import { HttpClientResponse } from "effect/unstable/http";
|
|
3
3
|
import { Media } from "../media.js";
|
|
4
4
|
import { AIError, ContentPolicyError, HttpContext, InvalidProviderOutputError, InvalidRequestError, ProviderInternalError, } from "../schema/index.js";
|
|
5
|
-
export const json = (value) => ({
|
|
5
|
+
export const json = (value, query) => ({
|
|
6
|
+
type: "json",
|
|
7
|
+
value,
|
|
8
|
+
query,
|
|
9
|
+
});
|
|
6
10
|
export const multipart = (value) => ({ type: "multipart", value });
|
|
7
11
|
export const inline = (input) => ({
|
|
8
12
|
kind: "inline",
|
|
@@ -12,6 +16,10 @@ export const queued = (input) => ({
|
|
|
12
16
|
kind: "queued",
|
|
13
17
|
...input,
|
|
14
18
|
});
|
|
19
|
+
export const stream = (input) => ({
|
|
20
|
+
kind: "stream",
|
|
21
|
+
...input,
|
|
22
|
+
});
|
|
15
23
|
// ---------------------------------------------------------------------------
|
|
16
24
|
// Response helpers
|
|
17
25
|
// ---------------------------------------------------------------------------
|
|
@@ -64,6 +72,19 @@ export const status = (table, raw, output) => {
|
|
|
64
72
|
return Effect.fail(output.invalid(`Unknown generation status "${raw}"`));
|
|
65
73
|
return Effect.succeed(normalized);
|
|
66
74
|
};
|
|
75
|
+
export const frameError = (route, message, body, cause) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body, cause }) });
|
|
76
|
+
export const incomplete = (route) => new AIError({
|
|
77
|
+
reason: new InvalidProviderOutputError({
|
|
78
|
+
route,
|
|
79
|
+
message: "The provider response ended unexpectedly.",
|
|
80
|
+
classification: "incomplete-stream",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
/** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
|
|
84
|
+
export const decodeFrame = (route, name, schema) => {
|
|
85
|
+
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
|
|
86
|
+
return (frame) => decode(frame).pipe(Effect.mapError((cause) => frameError(route, `${name} sent an invalid stream event`, frame, cause)));
|
|
87
|
+
};
|
|
67
88
|
/** A `url` asset whose provider-declared retention window starts now. */
|
|
68
89
|
export const expiringUrl = (url, retention, options) => Clock.currentTimeMillis.pipe(Effect.map((now) => Media.url(url, { ...options, expiresAt: now + Duration.toMillis(retention) })));
|
|
69
90
|
export * as MediaProtocol from "./media-protocol.js";
|
package/dist/route/media.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
2
|
import { Auth } from "./auth.js";
|
|
3
3
|
import { Endpoint } from "./endpoint.js";
|
|
4
4
|
import type { Interface } from "./executor-service.js";
|
|
@@ -39,6 +39,14 @@ export interface QueuedRoute<Request extends MediaRequest, Response> {
|
|
|
39
39
|
/** Rebuild a handle from a persisted `Generation.token`; fails typed when the token is not this route's. */
|
|
40
40
|
readonly resume: (model: MediaRequest["model"], token: unknown, execute: Execute) => Effect.Effect<Generation<Response>, AIError>;
|
|
41
41
|
}
|
|
42
|
+
/** One request whose response parses into events; `generate` runs the same stream and collects it. */
|
|
43
|
+
export interface StreamRoute<Request extends MediaRequest, Event, Response> {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly provider: ProviderID;
|
|
46
|
+
readonly protocol: string;
|
|
47
|
+
readonly stream: (request: Request, execute: Execute) => Stream.Stream<Event, AIError>;
|
|
48
|
+
readonly generate: (request: Request, execute: Execute) => Effect.Effect<Response, AIError>;
|
|
49
|
+
}
|
|
42
50
|
export interface Composition<Request extends MediaRequest> {
|
|
43
51
|
readonly id: string;
|
|
44
52
|
readonly provider: string | ProviderID;
|
|
@@ -53,6 +61,10 @@ export interface InlineInput<Request extends MediaRequest, Response> extends Com
|
|
|
53
61
|
export interface QueuedInput<Request extends MediaRequest, Response, Token> extends Composition<Request> {
|
|
54
62
|
readonly protocol: MediaProtocol.Queued<Request, Response, Token>;
|
|
55
63
|
}
|
|
64
|
+
export interface StreamInput<Request extends MediaRequest, Event, Response, Frame, State> extends Composition<MediaProtocol.Addressed<Request>> {
|
|
65
|
+
readonly protocol: MediaProtocol.Streamed<Request, Event, Frame, State>;
|
|
66
|
+
readonly collect: (events: ReadonlyArray<Event>) => Effect.Effect<Response, AIError>;
|
|
67
|
+
}
|
|
56
68
|
/**
|
|
57
69
|
* Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
|
|
58
70
|
* plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field
|
|
@@ -66,4 +78,6 @@ export declare const inline: <Request extends MediaRequest, Response>(input: Inl
|
|
|
66
78
|
* closed over by the resulting `Generation.Route`.
|
|
67
79
|
*/
|
|
68
80
|
export declare const queued: <Request extends MediaRequest, Response, Token>(input: QueuedInput<Request, Response, Token>) => QueuedRoute<Request, Response>;
|
|
81
|
+
/** Compose a streaming media protocol; `generate` runs the same stream in `generate` mode and folds it with `collect`. */
|
|
82
|
+
export declare const stream: <Request extends MediaRequest, Event, Response, Frame, State>(input: StreamInput<Request, Event, Response, Frame, State>) => StreamRoute<Request, Event, Response>;
|
|
69
83
|
export * as MediaRoute from "./media.js";
|
package/dist/route/media.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { Auth } from "./auth.js";
|
|
4
4
|
import { Endpoint } from "./endpoint.js";
|
|
5
|
+
import { RequestExecutor } from "./executor.js";
|
|
5
6
|
import { MediaProtocol } from "./media-protocol.js";
|
|
6
7
|
import { Generation } from "../generation.js";
|
|
7
8
|
import { ProviderShared } from "../protocols/shared.js";
|
|
8
|
-
import { AIError, HttpOptions, InvalidRequestError, ProviderID, mergeHttpOptions } from "../schema/index.js";
|
|
9
|
+
import { AIError, AIErrorReason, HttpOptions, InvalidRequestError, ProviderID, mergeHttpOptions, } from "../schema/index.js";
|
|
9
10
|
import { sanitizeSurrogates } from "../utils/sanitize.js";
|
|
10
11
|
/**
|
|
11
12
|
* Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
|
|
@@ -68,15 +69,48 @@ export const queued = (input) => {
|
|
|
68
69
|
});
|
|
69
70
|
return { id: input.id, provider: transport.provider, protocol: protocol.id, start, resume };
|
|
70
71
|
};
|
|
72
|
+
/** Compose a streaming media protocol; `generate` runs the same stream in `generate` mode and folds it with `collect`. */
|
|
73
|
+
export const stream = (input) => {
|
|
74
|
+
const transport = makeTransport(input);
|
|
75
|
+
const protocol = input.protocol;
|
|
76
|
+
const events = (request, execute, mode) => Stream.unwrap(Effect.gen(function* () {
|
|
77
|
+
const submitted = yield* transport.submit({ ...request, mode }, { unsupported: protocol.unsupported, from: protocol.body.from }, execute);
|
|
78
|
+
const http = RequestExecutor.responseHttp(submitted.response);
|
|
79
|
+
return Stream.suspend(() => {
|
|
80
|
+
// Parser state is local to one response, exactly like `Route.make`'s LLM stream loop.
|
|
81
|
+
let state = protocol.initial();
|
|
82
|
+
return protocol.frames(RequestExecutor.responseStream(submitted.response), submitted.context).pipe(Stream.mapEffect((frame) => protocol.step(state, frame).pipe(Effect.map(([next, output]) => {
|
|
83
|
+
state = next;
|
|
84
|
+
return output;
|
|
85
|
+
}))), Stream.flattenIterable, Stream.concat(Stream.suspend(() => Stream.fromIterableEffect(protocol.finish(state, { ...submitted.context, http })))), Stream.mapError((error) => error.reason.http !== undefined
|
|
86
|
+
? error
|
|
87
|
+
: new AIError({
|
|
88
|
+
reason: AIErrorReason.make({
|
|
89
|
+
...error.reason,
|
|
90
|
+
message: error.reason.message,
|
|
91
|
+
cause: error.reason.cause,
|
|
92
|
+
http,
|
|
93
|
+
}),
|
|
94
|
+
})));
|
|
95
|
+
});
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
id: input.id,
|
|
99
|
+
provider: transport.provider,
|
|
100
|
+
protocol: protocol.id,
|
|
101
|
+
stream: (request, execute) => events(request, execute, "stream"),
|
|
102
|
+
generate: (request, execute) => events(request, execute, "generate").pipe(Stream.runCollect, Effect.flatMap(input.collect)),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
71
105
|
// ---------------------------------------------------------------------------
|
|
72
|
-
// Transport plumbing shared by
|
|
106
|
+
// Transport plumbing shared by every kind
|
|
73
107
|
// ---------------------------------------------------------------------------
|
|
74
108
|
const makeTransport = (input) => {
|
|
75
109
|
const provider = ProviderID.make(input.provider);
|
|
76
110
|
const routeHttp = input.headers === undefined ? undefined : new HttpOptions({ headers: input.headers });
|
|
77
111
|
const authorize = Auth.toEffect(input.auth);
|
|
78
|
-
const withQuery = (url,
|
|
79
|
-
for (const [key, value] of Object.entries(
|
|
112
|
+
const withQuery = (url, query) => {
|
|
113
|
+
for (const [key, value] of Object.entries(query ?? {}))
|
|
80
114
|
url.searchParams.set(key, value);
|
|
81
115
|
return url;
|
|
82
116
|
};
|
|
@@ -91,7 +125,7 @@ const makeTransport = (input) => {
|
|
|
91
125
|
// Sanitize after merging so model-level overlays are covered; the model value is restored, not sanitized.
|
|
92
126
|
const resolved = { ...sanitizeSurrogates({ ...request, http }), model: request.model };
|
|
93
127
|
const body = yield* protocol.from(resolved);
|
|
94
|
-
const url = withQuery(Endpoint.render(input.endpoint, { request: resolved, body }), http);
|
|
128
|
+
const url = withQuery(withQuery(Endpoint.render(input.endpoint, { request: resolved, body }), body.type === "json" ? body.query : undefined), http?.query);
|
|
95
129
|
const encoded = body.type === "json" ? ProviderShared.encodeJson(body.value) : "[multipart/form-data]";
|
|
96
130
|
const baseHeaders = Headers.fromInput(http?.headers);
|
|
97
131
|
const headers = yield* authorize({
|
|
@@ -116,7 +150,7 @@ const makeTransport = (input) => {
|
|
|
116
150
|
// Provider-issued absolute URLs (fal `status_url`) are used as-is; everything else resolves against the base.
|
|
117
151
|
const url = withQuery(/^https?:\/\//.test(path)
|
|
118
152
|
? new URL(path)
|
|
119
|
-
: new URL(`${ProviderShared.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`), http);
|
|
153
|
+
: new URL(`${ProviderShared.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`), http?.query);
|
|
120
154
|
for (const [key, value] of Object.entries(input.endpoint.query ?? {}))
|
|
121
155
|
url.searchParams.set(key, value);
|
|
122
156
|
const base = Headers.fromInput(http?.headers);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Stream } from "effect";
|
|
2
|
+
import { RequestExecutor } from "./route/executor.js";
|
|
3
|
+
import type { AIError } from "./schema/index.js";
|
|
4
|
+
import type { SpeechEvent, SpeechOptions, SpeechRequestFor, SpeechResponse } from "./speech.js";
|
|
5
|
+
export interface Interface {
|
|
6
|
+
readonly generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError>;
|
|
7
|
+
readonly stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError>;
|
|
8
|
+
}
|
|
9
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/SpeechClient", Interface>;
|
|
10
|
+
export declare class Service extends Service_base {
|
|
11
|
+
}
|
|
12
|
+
export declare const generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError, Service>;
|
|
13
|
+
export declare const stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError, Service>;
|
|
14
|
+
export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
|
|
15
|
+
export declare const SpeechClient: {
|
|
16
|
+
readonly Service: typeof Service;
|
|
17
|
+
readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
|
|
18
|
+
readonly generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError, Service>;
|
|
19
|
+
readonly stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError, Service>;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Stream } from "effect";
|
|
2
|
+
import { RequestExecutor } from "./route/executor.js";
|
|
3
|
+
export class Service extends Context.Service()("@opencode/SpeechClient") {
|
|
4
|
+
}
|
|
5
|
+
export const generate = (request) => Effect.gen(function* () {
|
|
6
|
+
const client = yield* Service;
|
|
7
|
+
return yield* client.generate(request);
|
|
8
|
+
});
|
|
9
|
+
export const stream = (request) => Stream.unwrap(Effect.gen(function* () {
|
|
10
|
+
const client = yield* Service;
|
|
11
|
+
return client.stream(request);
|
|
12
|
+
}));
|
|
13
|
+
export const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
14
|
+
const executor = yield* RequestExecutor.Service;
|
|
15
|
+
return Service.of({
|
|
16
|
+
generate: (request) => request.model.route.generate(request, executor.execute),
|
|
17
|
+
stream: (request) => request.model.route.stream(request, executor.execute),
|
|
18
|
+
});
|
|
19
|
+
}));
|
|
20
|
+
export const SpeechClient = {
|
|
21
|
+
Service,
|
|
22
|
+
layer,
|
|
23
|
+
generate,
|
|
24
|
+
stream,
|
|
25
|
+
};
|