@opencode/ai 0.0.0-dev-20040 → 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.
Files changed (46) hide show
  1. package/README.md +86 -1
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +3 -0
  4. package/dist/media-model.d.ts +3 -3
  5. package/dist/media-model.js +3 -3
  6. package/dist/promise.d.ts +111 -3
  7. package/dist/promise.js +9 -1
  8. package/dist/protocols/bedrock-converse.js +2 -8
  9. package/dist/protocols/cartesia-speech.d.ts +127 -0
  10. package/dist/protocols/cartesia-speech.js +126 -0
  11. package/dist/protocols/deepgram-speech.d.ts +119 -0
  12. package/dist/protocols/deepgram-speech.js +90 -0
  13. package/dist/protocols/elevenlabs-speech.d.ts +122 -0
  14. package/dist/protocols/elevenlabs-speech.js +115 -0
  15. package/dist/protocols/google-speech.d.ts +160 -0
  16. package/dist/protocols/google-speech.js +122 -0
  17. package/dist/protocols/openai-speech.d.ts +116 -0
  18. package/dist/protocols/openai-speech.js +98 -0
  19. package/dist/protocols/utils/speech-stream.d.ts +49 -0
  20. package/dist/protocols/utils/speech-stream.js +67 -0
  21. package/dist/providers/cartesia.d.ts +24 -0
  22. package/dist/providers/cartesia.js +22 -0
  23. package/dist/providers/deepgram.d.ts +24 -0
  24. package/dist/providers/deepgram.js +28 -0
  25. package/dist/providers/elevenlabs.d.ts +24 -0
  26. package/dist/providers/elevenlabs.js +28 -0
  27. package/dist/providers/google.d.ts +5 -0
  28. package/dist/providers/google.js +3 -0
  29. package/dist/providers/index.d.ts +3 -0
  30. package/dist/providers/index.js +3 -0
  31. package/dist/providers/openai.d.ts +5 -0
  32. package/dist/providers/openai.js +6 -1
  33. package/dist/route/framing.d.ts +5 -1
  34. package/dist/route/framing.js +9 -0
  35. package/dist/route/media-protocol.d.ts +35 -2
  36. package/dist/route/media-protocol.js +22 -1
  37. package/dist/route/media.d.ts +15 -1
  38. package/dist/route/media.js +41 -7
  39. package/dist/speech-client.d.ts +21 -0
  40. package/dist/speech-client.js +25 -0
  41. package/dist/speech.d.ts +1150 -0
  42. package/dist/speech.js +119 -0
  43. package/dist/utils/bytes.d.ts +1 -0
  44. package/dist/utils/bytes.js +10 -0
  45. package/dist/utils/media-type.js +8 -1
  46. package/package.json +3 -3
@@ -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";
@@ -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>;
@@ -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 image = (modelID) => OpenAIImages.model({
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;
@@ -1,4 +1,4 @@
1
- import type { Stream } from "effect";
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";
@@ -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) => ({ type: "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";
@@ -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";
@@ -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 both kinds
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, http) => {
79
- for (const [key, value] of Object.entries(http?.query ?? {}))
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
+ };