@opencode/ai 0.0.0-dev-20120 → 0.0.0-dev-20123

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 (128) hide show
  1. package/dist/experimental/evaluation.js +1 -1
  2. package/dist/generation.d.ts +1 -7
  3. package/dist/generation.js +1 -11
  4. package/dist/image.d.ts +2 -2
  5. package/dist/image.js +2 -2
  6. package/dist/llm.js +1 -1
  7. package/dist/media-model.d.ts +0 -2
  8. package/dist/media-model.js +0 -2
  9. package/dist/protocols/alibaba-messages.d.ts +1 -1
  10. package/dist/protocols/anthropic-messages.d.ts +34 -34
  11. package/dist/protocols/assemblyai-transcription.d.ts +2 -1
  12. package/dist/protocols/assemblyai-transcription.js +10 -14
  13. package/dist/protocols/bfl-images.js +15 -31
  14. package/dist/protocols/cartesia-speech.d.ts +2 -2
  15. package/dist/protocols/cartesia-speech.js +10 -16
  16. package/dist/protocols/deepgram-speech.d.ts +3 -3
  17. package/dist/protocols/deepgram-speech.js +7 -11
  18. package/dist/protocols/deepgram-transcription.d.ts +2 -1
  19. package/dist/protocols/deepgram-transcription.js +9 -13
  20. package/dist/protocols/elevenlabs-speech.d.ts +3 -3
  21. package/dist/protocols/elevenlabs-speech.js +9 -13
  22. package/dist/protocols/fal-images.d.ts +2 -1
  23. package/dist/protocols/fal-images.js +16 -29
  24. package/dist/protocols/fal-video.d.ts +2 -2
  25. package/dist/protocols/fal-video.js +9 -24
  26. package/dist/protocols/gemini.d.ts +13 -13
  27. package/dist/protocols/google-images.d.ts +3 -3
  28. package/dist/protocols/google-images.js +11 -21
  29. package/dist/protocols/google-speech.js +7 -13
  30. package/dist/protocols/google-transcription.d.ts +2 -1
  31. package/dist/protocols/google-transcription.js +8 -20
  32. package/dist/protocols/google-video.d.ts +2 -2
  33. package/dist/protocols/google-video.js +14 -30
  34. package/dist/protocols/meta-images.d.ts +3 -4
  35. package/dist/protocols/meta-images.js +12 -21
  36. package/dist/protocols/meta-messages.d.ts +5 -5
  37. package/dist/protocols/meta-responses.js +5 -2
  38. package/dist/protocols/mistral-chat.js +6 -3
  39. package/dist/protocols/openai-images.d.ts +4 -5
  40. package/dist/protocols/openai-images.js +24 -55
  41. package/dist/protocols/openai-responses.js +8 -2
  42. package/dist/protocols/openai-speech.js +7 -11
  43. package/dist/protocols/openai-transcription.js +21 -29
  44. package/dist/protocols/replicate-images.js +11 -17
  45. package/dist/protocols/runway-video.d.ts +3 -3
  46. package/dist/protocols/runway-video.js +10 -16
  47. package/dist/protocols/shared.d.ts +5 -17
  48. package/dist/protocols/shared.js +3 -39
  49. package/dist/protocols/stability-images.d.ts +2 -1
  50. package/dist/protocols/stability-images.js +24 -36
  51. package/dist/protocols/utils/fal-queue.d.ts +1 -3
  52. package/dist/protocols/utils/fal-queue.js +4 -6
  53. package/dist/protocols/utils/media-input.d.ts +15 -1
  54. package/dist/protocols/utils/media-input.js +22 -0
  55. package/dist/protocols/utils/speech-stream.d.ts +3 -3
  56. package/dist/protocols/utils/speech-stream.js +1 -4
  57. package/dist/protocols/utils/tool-schema.js +19 -6
  58. package/dist/protocols/xai-images.d.ts +4 -4
  59. package/dist/protocols/xai-images.js +12 -28
  60. package/dist/protocols/xai-video.js +12 -18
  61. package/dist/protocols/zai-images.d.ts +2 -2
  62. package/dist/protocols/zai-images.js +8 -11
  63. package/dist/protocols/zai-messages.d.ts +1 -1
  64. package/dist/providers/alibaba.d.ts +1 -1
  65. package/dist/providers/anthropic-compatible.d.ts +5 -5
  66. package/dist/providers/anthropic.d.ts +5 -5
  67. package/dist/providers/assemblyai.d.ts +1 -1
  68. package/dist/providers/assemblyai.js +5 -10
  69. package/dist/providers/azure.js +2 -2
  70. package/dist/providers/black-forest-labs.d.ts +1 -1
  71. package/dist/providers/black-forest-labs.js +5 -10
  72. package/dist/providers/cartesia.d.ts +1 -1
  73. package/dist/providers/cartesia.js +5 -10
  74. package/dist/providers/cloudflare-ai-gateway.d.ts +10 -10
  75. package/dist/providers/deepgram.d.ts +1 -1
  76. package/dist/providers/deepgram.js +6 -12
  77. package/dist/providers/elevenlabs.d.ts +1 -1
  78. package/dist/providers/elevenlabs.js +5 -10
  79. package/dist/providers/fal.d.ts +1 -1
  80. package/dist/providers/fal.js +5 -12
  81. package/dist/providers/google-vertex-messages.d.ts +5 -5
  82. package/dist/providers/google-vertex.d.ts +3 -3
  83. package/dist/providers/google.d.ts +3 -3
  84. package/dist/providers/google.js +7 -12
  85. package/dist/providers/meta.d.ts +8 -8
  86. package/dist/providers/meta.js +4 -8
  87. package/dist/providers/minimax.d.ts +5 -5
  88. package/dist/providers/moonshot.d.ts +5 -5
  89. package/dist/providers/openai.d.ts +7 -8
  90. package/dist/providers/openai.js +10 -11
  91. package/dist/providers/opencode-zen.js +1 -1
  92. package/dist/providers/openrouter.d.ts +6 -7
  93. package/dist/providers/openrouter.js +1 -1
  94. package/dist/providers/replicate.d.ts +1 -1
  95. package/dist/providers/replicate.js +5 -10
  96. package/dist/providers/runway.d.ts +1 -1
  97. package/dist/providers/runway.js +5 -10
  98. package/dist/providers/stability.d.ts +1 -1
  99. package/dist/providers/stability.js +6 -11
  100. package/dist/providers/typesafe-ai.js +1 -1
  101. package/dist/providers/vercel-ai-gateway.js +1 -1
  102. package/dist/providers/xai.js +5 -10
  103. package/dist/providers/zai-coding-plan.d.ts +1 -1
  104. package/dist/providers/zai.js +4 -8
  105. package/dist/route/client.d.ts +0 -1
  106. package/dist/route/client.js +1 -6
  107. package/dist/route/endpoint.d.ts +1 -0
  108. package/dist/route/endpoint.js +2 -2
  109. package/dist/route/framing.d.ts +10 -1
  110. package/dist/route/framing.js +45 -5
  111. package/dist/route/media-protocol.d.ts +42 -35
  112. package/dist/route/media-protocol.js +67 -62
  113. package/dist/route/media.d.ts +6 -2
  114. package/dist/route/media.js +40 -29
  115. package/dist/schema/options.d.ts +6 -3
  116. package/dist/schema/options.js +7 -3
  117. package/dist/speech.d.ts +2 -2
  118. package/dist/speech.js +2 -2
  119. package/dist/transcription.js +2 -2
  120. package/dist/utils/json.d.ts +4 -0
  121. package/dist/utils/json.js +4 -0
  122. package/dist/video.d.ts +2 -2
  123. package/dist/video.js +2 -2
  124. package/package.json +3 -3
  125. package/dist/protocols/utils/meta-image.d.ts +0 -2
  126. package/dist/protocols/utils/meta-image.js +0 -13
  127. package/dist/protocols/utils/openai-image.d.ts +0 -5
  128. package/dist/protocols/utils/openai-image.js +0 -18
@@ -1,16 +1,56 @@
1
- import { Stream } from "effect";
2
- import * as ProviderShared from "../protocols/shared.js";
1
+ import { Effect, Stream } from "effect";
2
+ import { makeParser } from "effect/unstable/encoding/Sse";
3
+ import { AIError, InvalidProviderOutputError } from "../schema/index.js";
4
+ /**
5
+ * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
6
+ * decoder, optionally filters named events, and drops empty events and known
7
+ * keepalives that proxies send as data. `[DONE]` is dropped by default or
8
+ * retained for protocols that use it as their stream boundary. Retry control events are ignored without
9
+ * interrupting the stream. Decoder failures become provider output errors so
10
+ * the public error channel stays `AIError`.
11
+ */
12
+ export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
13
+ const output = [];
14
+ return {
15
+ output,
16
+ parser: makeParser((event) => {
17
+ if (event._tag === "Event")
18
+ output.push(event);
19
+ }),
20
+ };
21
+ }, (state, chunk) => Effect.gen(function* () {
22
+ const error = state.parser.feed(chunk);
23
+ if (error)
24
+ return yield* new AIError({
25
+ reason: new InvalidProviderOutputError({
26
+ route: "sse",
27
+ message: error.message,
28
+ body: chunk,
29
+ cause: error,
30
+ }),
31
+ });
32
+ return [state, state.output.splice(0)];
33
+ })), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
34
+ event.data.length > 0 &&
35
+ // Some OpenAI-compatible proxies serialize an empty flush as a bare
36
+ // `data: null`, between events or after `[DONE]`. No protocol has a
37
+ // null event, so it carries nothing and must not abort the stream.
38
+ event.data !== "null" &&
39
+ // Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
40
+ // keepalive comment as `data: : keepalive` while reasoning.
41
+ event.data !== ": keepalive" &&
42
+ (event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
3
43
  /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
4
- export const sse = { id: "sse", frame: ProviderShared.sseFraming };
44
+ export const sse = { id: "sse", frame: sseFraming };
5
45
  /** Server-Sent Events framing that retains the conventional `[DONE]` sentinel. */
6
46
  export const sseWithDone = {
7
47
  id: "sse",
8
- frame: (bytes) => ProviderShared.sseFraming(bytes, undefined, true),
48
+ frame: (bytes) => sseFraming(bytes, undefined, true),
9
49
  };
10
50
  /** SSE framing restricted to protocol-recognized event names. */
11
51
  export const sseEvents = (events) => ({
12
52
  id: "sse",
13
- frame: (bytes) => ProviderShared.sseFraming(bytes, events),
53
+ frame: (bytes) => sseFraming(bytes, events),
14
54
  });
15
55
  export const lines = {
16
56
  id: "lines",
@@ -3,7 +3,7 @@ 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
- import { AIError, HttpContext } from "../schema/index.js";
6
+ import { AIError, HttpContext, ProviderID } from "../schema/index.js";
7
7
  /** Array values become repeated parameters (`keyterm=a&keyterm=b`). */
8
8
  export type Query = Readonly<Record<string, string | ReadonlyArray<string>>>;
9
9
  /** `query` is appended to the endpoint URL before the route and caller `http.query` overlays. */
@@ -34,7 +34,7 @@ export interface DecodeContext<Request> {
34
34
  export interface Inline<Request, Response> {
35
35
  readonly kind: "inline";
36
36
  readonly id: string;
37
- readonly name: string;
37
+ readonly provider: ProviderID;
38
38
  /** Common request fields this protocol cannot lower; the route rejects them before `body.from` runs. */
39
39
  readonly unsupported?: ReadonlyArray<keyof Request & string>;
40
40
  readonly body: {
@@ -44,7 +44,7 @@ export interface Inline<Request, Response> {
44
44
  readonly decode: (response: HttpClientResponse.HttpClientResponse, context: DecodeContext<Request>) => Effect.Effect<Response, AIError>;
45
45
  };
46
46
  }
47
- export declare const inline: <Request, Response>(input: Omit<Inline<Request, Response>, "kind">) => Inline<Request, Response>;
47
+ export declare const inline: <Request, Response>(route: Identity, input: Omit<Inline<Request, Response>, "kind" | "id" | "provider">) => Inline<Request, Response>;
48
48
  /** What `start` learned from the submission response: the route-owned handle plus the first observation. */
49
49
  export interface Started<Token> {
50
50
  readonly token: Token;
@@ -70,7 +70,7 @@ export interface PollContext<Token> {
70
70
  export interface Queued<Request, Response, Token> {
71
71
  readonly kind: "queued";
72
72
  readonly id: string;
73
- readonly name: string;
73
+ readonly provider: ProviderID;
74
74
  /** Common request fields this protocol cannot lower; the route rejects them before `start.body.from` runs. */
75
75
  readonly unsupported?: ReadonlyArray<keyof Request & string>;
76
76
  /** Serializable handle. `Generation.token` carries the encoded form so it can be persisted and resumed elsewhere. */
@@ -95,7 +95,7 @@ export interface Queued<Request, Response, Token> {
95
95
  readonly path: (token: Token) => string;
96
96
  };
97
97
  }
98
- export declare const queued: <Request, Response, Token>(input: Omit<Queued<Request, Response, Token>, "kind">) => Queued<Request, Response, Token>;
98
+ export declare const queued: <Request, Response, Token>(route: Identity, input: Omit<Queued<Request, Response, Token>, "kind" | "id" | "provider">) => Queued<Request, Response, Token>;
99
99
  export type Mode = "generate" | "stream";
100
100
  export type Addressed<Request> = Request & {
101
101
  readonly mode: Mode;
@@ -110,7 +110,7 @@ export interface ResponseContext<Request> extends DecodeContext<Addressed<Reques
110
110
  export interface Streamed<Request, Event, Frame, State> {
111
111
  readonly kind: "stream";
112
112
  readonly id: string;
113
- readonly name: string;
113
+ readonly provider: ProviderID;
114
114
  /** Common request fields this protocol cannot lower; the route rejects them before `body.from` runs. */
115
115
  readonly unsupported?: ReadonlyArray<keyof Request & string>;
116
116
  readonly body: {
@@ -122,37 +122,44 @@ export interface Streamed<Request, Event, Frame, State> {
122
122
  /** Emit exactly one terminal event, or fail when the provider stopped before completing. */
123
123
  readonly finish: (state: State, context: ResponseContext<Request>) => Effect.Effect<ReadonlyArray<Event>, AIError>;
124
124
  }
125
- export declare const stream: <Request, Event, Frame, State>(input: Omit<Streamed<Request, Event, Frame, State>, "kind">) => Streamed<Request, Event, Frame, State>;
126
- /**
127
- * Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a
128
- * malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is
129
- * provider-side, `cancelled`/`expired` mean the result will never exist); `contentPolicy` is a moderated result.
130
- */
131
- export declare const text: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
132
- body: string;
133
- http: HttpContext;
134
- invalid: (message: string, cause?: unknown) => AIError;
135
- ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
136
- contentPolicy: (message: string) => AIError;
137
- }, AIError, never>;
138
- export type Output = Effect.Success<ReturnType<typeof text>>;
139
- /** Read and Schema-decode a JSON body. Decode failures keep the raw body as `reason.body`. */
140
- export declare const decodeJson: <A>(route: string, name: string, schema: Schema.Codec<A, unknown>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
141
- value: A;
142
- body: string;
143
- http: HttpContext;
144
- invalid: (message: string, cause?: unknown) => AIError;
145
- ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
146
- contentPolicy: (message: string) => AIError;
147
- }, AIError, never>;
148
- /** Decode a submission response into the token and first snapshot. */
149
- 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>;
125
+ export declare const stream: <Request, Event, Frame, State>(route: Identity, input: Omit<Streamed<Request, Event, Frame, State>, "kind" | "id" | "provider">) => Streamed<Request, Event, Frame, State>;
126
+ /** One protocol's route id, display name, and provider, with the decoders and errors that carry them. */
127
+ export declare const identity: (input: {
128
+ readonly id: string;
129
+ readonly name: string;
130
+ readonly provider: string;
131
+ }) => {
132
+ id: string;
133
+ name: string;
134
+ provider: string & import("effect/Brand").Brand<"AI.ProviderID">;
135
+ text: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
136
+ body: string;
137
+ http: HttpContext;
138
+ invalid: (message: string, cause?: unknown) => AIError;
139
+ ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
140
+ contentPolicy: (message: string) => AIError;
141
+ }, AIError, never>;
142
+ decodeJson: <A>(schema: Schema.Codec<A, unknown>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{
143
+ value: A;
144
+ body: string;
145
+ http: HttpContext;
146
+ invalid: (message: string, cause?: unknown) => AIError;
147
+ ended: (status: Exclude<Status, "queued" | "running" | "completed">, message: string) => AIError;
148
+ contentPolicy: (message: string) => AIError;
149
+ }, AIError, never>;
150
+ /** Decode a submission response into the token and first snapshot. */
151
+ decodeStarted: <A, Token>(schema: Schema.Codec<A, unknown>, started: (value: A) => Started<Token>) => (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<Started<Token>, AIError, never>;
152
+ /** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
153
+ decodeFrame: <A>(schema: Schema.Codec<A, unknown>) => (frame: string) => Effect.Effect<A, AIError, never>;
154
+ /** A stream-time failure; the frame stays on `reason.body`. */
155
+ frameError: (message: string, body?: string, cause?: unknown) => AIError;
156
+ incomplete: () => AIError;
157
+ unsupported: (operation: string, message: string) => AIError;
158
+ };
159
+ export type Identity = ReturnType<typeof identity>;
160
+ export type Output = Effect.Success<ReturnType<Identity["text"]>>;
150
161
  /** Map a provider status string through the protocol's table; unknown values are an invalid provider document. */
151
162
  export declare const status: <Table extends Record<string, Status>>(table: Table, raw: string, output: Output) => Effect.Effect<Status, AIError>;
152
- export declare const frameError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
153
- export declare const incomplete: (route: string) => AIError;
154
- /** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
155
- export declare const decodeFrame: <A>(route: string, name: string, schema: Schema.Codec<A, unknown>) => (frame: string) => Effect.Effect<A, AIError, never>;
156
163
  /** A `url` asset whose provider-declared retention window starts now. */
157
164
  export declare const expiringUrl: (url: string, retention: Duration.Duration, options?: Parameters<typeof Media.url>[1]) => Effect.Effect<Media.Asset, never, never>;
158
165
  export * as MediaProtocol from "./media-protocol.js";
@@ -1,7 +1,7 @@
1
1
  import { Clock, Duration, Effect, Schema } from "effect";
2
2
  import { HttpClientResponse } from "effect/unstable/http";
3
3
  import { Media } from "../media.js";
4
- import { AIError, ContentPolicyError, HttpContext, InvalidProviderOutputError, InvalidRequestError, ProviderInternalError, } from "../schema/index.js";
4
+ import { AIError, ContentPolicyError, HttpContext, InvalidProviderOutputError, InvalidRequestError, ProviderID, ProviderInternalError, UnsupportedOperationError, } from "../schema/index.js";
5
5
  export const json = (value, query) => ({
6
6
  type: "json",
7
7
  value,
@@ -14,62 +14,80 @@ export const binary = (value, contentType, query) => ({
14
14
  contentType,
15
15
  query,
16
16
  });
17
- export const inline = (input) => ({
18
- kind: "inline",
19
- ...input,
20
- });
21
- export const queued = (input) => ({
22
- kind: "queued",
23
- ...input,
24
- });
25
- export const stream = (input) => ({
26
- kind: "stream",
27
- ...input,
28
- });
17
+ export const inline = (route, input) => ({ kind: "inline", id: route.id, provider: route.provider, ...input });
18
+ export const queued = (route, input) => ({ kind: "queued", id: route.id, provider: route.provider, ...input });
19
+ export const stream = (route, input) => ({ kind: "stream", id: route.id, provider: route.provider, ...input });
29
20
  // ---------------------------------------------------------------------------
30
21
  // Response helpers
31
22
  // ---------------------------------------------------------------------------
32
23
  const context = (response) => new HttpContext({ url: response.request.url, status: response.status, headers: response.headers });
33
- /**
34
- * Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a
35
- * malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is
36
- * provider-side, `cancelled`/`expired` mean the result will never exist); `contentPolicy` is a moderated result.
37
- */
38
- export const text = Effect.fn("MediaProtocol.text")(function* (route, name, response) {
39
- const http = context(response);
40
- const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
41
- reason: new InvalidProviderOutputError({
42
- route,
43
- message: `Failed to read the ${name} response`,
24
+ /** One protocol's route id, display name, and provider, with the decoders and errors that carry them. */
25
+ export const identity = (input) => {
26
+ const provider = ProviderID.make(input.provider);
27
+ const frameError = (message, body, cause) => new AIError({ reason: new InvalidProviderOutputError({ route: input.id, message, body, cause }) });
28
+ /**
29
+ * Read a text body while retaining the original payload and HTTP context on every downstream error. `invalid` is a
30
+ * malformed provider document; `ended` is a generation that reached a terminal status without output (`failed` is
31
+ * provider-side, `cancelled`/`expired` mean the result will never exist); `contentPolicy` is a moderated result.
32
+ */
33
+ const text = Effect.fn("MediaProtocol.text")(function* (response) {
34
+ const http = context(response);
35
+ const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({
36
+ reason: new InvalidProviderOutputError({
37
+ route: input.id,
38
+ message: `Failed to read the ${input.name} response`,
39
+ http,
40
+ cause,
41
+ }),
42
+ })));
43
+ return {
44
+ body,
44
45
  http,
45
- cause,
46
- }),
47
- })));
46
+ invalid: (message, cause) => new AIError({ reason: new InvalidProviderOutputError({ route: input.id, message, body, http, cause }) }),
47
+ ended: (status, message) => new AIError({
48
+ reason: status === "failed"
49
+ ? new ProviderInternalError({ message, body, http })
50
+ : new InvalidRequestError({ message, body, http }),
51
+ }),
52
+ contentPolicy: (message) => new AIError({ reason: new ContentPolicyError({ message, body, http }) }),
53
+ };
54
+ });
55
+ /** Read and Schema-decode a JSON body. Decode failures keep the raw body as `reason.body`. */
56
+ const decodeJson = (schema) => {
57
+ const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
58
+ return Effect.fn("MediaProtocol.decodeJson")(function* (response) {
59
+ const output = yield* text(response);
60
+ const value = yield* decode(output.body).pipe(Effect.mapError((cause) => output.invalid(`${input.name} returned an invalid response`, cause)));
61
+ return { ...output, value };
62
+ });
63
+ };
48
64
  return {
49
- body,
50
- http,
51
- invalid: (message, cause) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body, http, cause }) }),
52
- ended: (status, message) => new AIError({
53
- reason: status === "failed"
54
- ? new ProviderInternalError({ message, body, http })
55
- : new InvalidRequestError({ message, body, http }),
65
+ id: input.id,
66
+ name: input.name,
67
+ provider,
68
+ text,
69
+ decodeJson,
70
+ /** Decode a submission response into the token and first snapshot. */
71
+ decodeStarted: (schema, started) => {
72
+ const decode = decodeJson(schema);
73
+ return (response) => decode(response).pipe(Effect.map((output) => started(output.value)));
74
+ },
75
+ /** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
76
+ decodeFrame: (schema) => {
77
+ const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
78
+ return (frame) => decode(frame).pipe(Effect.mapError((cause) => frameError(`${input.name} sent an invalid stream event`, frame, cause)));
79
+ },
80
+ /** A stream-time failure; the frame stays on `reason.body`. */
81
+ frameError,
82
+ incomplete: () => new AIError({
83
+ reason: new InvalidProviderOutputError({
84
+ route: input.id,
85
+ message: "The provider response ended unexpectedly.",
86
+ classification: "incomplete-stream",
87
+ }),
56
88
  }),
57
- contentPolicy: (message) => new AIError({ reason: new ContentPolicyError({ message, body, http }) }),
89
+ unsupported: (operation, message) => new AIError({ reason: new UnsupportedOperationError({ operation, provider, route: input.id, message }) }),
58
90
  };
59
- });
60
- /** Read and Schema-decode a JSON body. Decode failures keep the raw body as `reason.body`. */
61
- export const decodeJson = (route, name, schema) => {
62
- const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
63
- return Effect.fn("MediaProtocol.decodeJson")(function* (response) {
64
- const output = yield* text(route, name, response);
65
- const value = yield* decode(output.body).pipe(Effect.mapError((cause) => output.invalid(`${name} returned an invalid response`, cause)));
66
- return { ...output, value };
67
- });
68
- };
69
- /** Decode a submission response into the token and first snapshot. */
70
- export const decodeStarted = (route, name, schema, started) => {
71
- const decode = decodeJson(route, name, schema);
72
- return (response) => decode(response).pipe(Effect.map((output) => started(output.value)));
73
91
  };
74
92
  /** Map a provider status string through the protocol's table; unknown values are an invalid provider document. */
75
93
  export const status = (table, raw, output) => {
@@ -78,19 +96,6 @@ export const status = (table, raw, output) => {
78
96
  return Effect.fail(output.invalid(`Unknown generation status "${raw}"`));
79
97
  return Effect.succeed(normalized);
80
98
  };
81
- export const frameError = (route, message, body, cause) => new AIError({ reason: new InvalidProviderOutputError({ route, message, body, cause }) });
82
- export const incomplete = (route) => new AIError({
83
- reason: new InvalidProviderOutputError({
84
- route,
85
- message: "The provider response ended unexpectedly.",
86
- classification: "incomplete-stream",
87
- }),
88
- });
89
- /** Schema-decode one JSON stream frame. Decode failures keep the frame as `reason.body`. */
90
- export const decodeFrame = (route, name, schema) => {
91
- const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(schema));
92
- return (frame) => decode(frame).pipe(Effect.mapError((cause) => frameError(route, `${name} sent an invalid stream event`, frame, cause)));
93
- };
94
99
  /** A `url` asset whose provider-declared retention window starts now. */
95
100
  export const expiringUrl = (url, retention, options) => Clock.currentTimeMillis.pipe(Effect.map((now) => Media.url(url, { ...options, expiresAt: now + Duration.toMillis(retention) })));
96
101
  export * as MediaProtocol from "./media-protocol.js";
@@ -23,6 +23,12 @@ export interface ModelInput {
23
23
  readonly headers?: Record<string, string>;
24
24
  readonly http?: HttpOptions;
25
25
  }
26
+ /** A provider facade's `configure(...)` input as the `ModelInput` every media selector shares, minus the model id. */
27
+ export declare const deployment: (input: {
28
+ readonly baseURL?: string;
29
+ readonly headers?: Record<string, string>;
30
+ readonly http?: HttpOptions.Input;
31
+ }, auth: Auth.Definition) => Omit<ModelInput, "id">;
26
32
  /** One request, one response. */
27
33
  export interface Route<Request extends MediaRequest, Response> {
28
34
  readonly kind: "inline";
@@ -52,8 +58,6 @@ export interface StreamRoute<Request extends MediaRequest, Event, Response> {
52
58
  }
53
59
  export type AnyRoute<Request extends MediaRequest, Event, Response> = Route<Request, Response> | StreamRoute<Request, Event, Response> | QueuedRoute<Request, Response>;
54
60
  export interface Composition<Request extends MediaRequest> {
55
- readonly id: string;
56
- readonly provider: string | ProviderID;
57
61
  readonly endpoint: Endpoint.Definition<MediaProtocol.Body, Request>;
58
62
  readonly auth: Auth.Definition;
59
63
  /** Deployment headers applied before transport authentication. */
@@ -2,13 +2,20 @@ 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 { Service as RequestExecutorService } from "./executor-service.js";
5
+ import { RequestExecutorService } from "./executor-service.js";
6
6
  import { RequestExecutor } from "./executor.js";
7
7
  import { MediaProtocol } from "./media-protocol.js";
8
- import { Generation, resultEvents, } from "../generation.js";
9
- import { ProviderShared } from "../protocols/shared.js";
10
- import { AIError, AIErrorReason, HttpOptions, InvalidRequestError, ProviderID, mergeHttpOptions, } from "../schema/index.js";
8
+ import { Generation, resultEvents } from "../generation.js";
9
+ import { AIError, AIErrorReason, HttpOptions, InvalidRequestError, ProviderID, UnsupportedOperationError, mergeHttpOptions, } from "../schema/index.js";
10
+ import { encodeJson } from "../utils/json.js";
11
11
  import { sanitizeSurrogates } from "../utils/sanitize.js";
12
+ /** A provider facade's `configure(...)` input as the `ModelInput` every media selector shares, minus the model id. */
13
+ export const deployment = (input, auth) => ({
14
+ auth,
15
+ baseURL: input.baseURL,
16
+ headers: input.headers,
17
+ http: HttpOptions.make(input.http),
18
+ });
12
19
  /**
13
20
  * Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
14
21
  * plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field
@@ -19,8 +26,8 @@ export const inline = (input) => {
19
26
  const transport = makeTransport(input);
20
27
  return {
21
28
  kind: "inline",
22
- id: input.id,
23
- provider: transport.provider,
29
+ id: input.protocol.id,
30
+ provider: input.protocol.provider,
24
31
  protocol: input.protocol.id,
25
32
  generate: Effect.fn(`MediaRoute.generate`)(function* (request, execute) {
26
33
  const submitted = yield* transport.submit(request, { unsupported: input.protocol.unsupported, from: input.protocol.body.from }, execute);
@@ -45,14 +52,13 @@ export const queued = (input) => {
45
52
  .call("GET", operation.path(token), http, execute)
46
53
  .pipe(Effect.flatMap((sent) => operation.decode(sent.response, { token, auth: sent.auth, materialize })));
47
54
  const cancel = protocol.cancel;
48
- const route = {
55
+ return {
49
56
  status: poll(protocol.status),
50
57
  result: poll(protocol.result),
51
58
  cancel: cancel === undefined
52
59
  ? undefined
53
60
  : transport.call(cancel.method, cancel.path(token), http, execute).pipe(Effect.asVoid),
54
61
  };
55
- return route;
56
62
  };
57
63
  const start = Effect.fn("MediaRoute.start")(function* (request, execute) {
58
64
  const submitted = yield* transport.submit(request, { unsupported: protocol.unsupported, prepare: protocol.start.prepare, from: protocol.start.body.from }, execute);
@@ -63,14 +69,14 @@ export const queued = (input) => {
63
69
  const resume = Effect.fn("MediaRoute.resume")(function* (model, raw, execute) {
64
70
  const token = yield* decodeToken(raw).pipe(Effect.mapError((cause) => new AIError({
65
71
  reason: new InvalidRequestError({
66
- message: `${input.id} cannot resume a generation from this token`,
72
+ message: `${protocol.id} cannot resume a generation from this token`,
67
73
  cause,
68
74
  }),
69
75
  })));
70
76
  const route = generationRoute(token, transport.http(model), execute);
71
77
  return new Generation(route, encodeToken(token), yield* route.status);
72
78
  });
73
- return { kind: "queued", id: input.id, provider: transport.provider, protocol: protocol.id, start, resume };
79
+ return { kind: "queued", id: protocol.id, provider: protocol.provider, protocol: protocol.id, start, resume };
74
80
  };
75
81
  /** Compose a streaming media protocol; `generate` runs the same stream in `generate` mode and folds it with `collect`. */
76
82
  export const stream = (input) => {
@@ -99,19 +105,21 @@ export const stream = (input) => {
99
105
  }));
100
106
  return {
101
107
  kind: "stream",
102
- id: input.id,
103
- provider: transport.provider,
108
+ id: protocol.id,
109
+ provider: protocol.provider,
104
110
  protocol: protocol.id,
105
111
  stream: (request, execute) => events(request, execute, "stream"),
106
112
  generate: (request, execute) => events(request, execute, "generate").pipe(Stream.runCollect, Effect.flatMap(input.collect)),
107
113
  };
108
114
  };
109
115
  export const dispatch = (input) => {
110
- const notQueued = (route, operation) => ProviderShared.unsupportedOperation({
111
- operation: `${input.modality}.${operation}`,
112
- provider: route.provider,
113
- route: route.id,
114
- message: `${route.provider}/${route.id} is not a queued route; use generate or stream`,
116
+ const notQueued = (route, operation) => new AIError({
117
+ reason: new UnsupportedOperationError({
118
+ operation: `${input.modality}.${operation}`,
119
+ provider: route.provider,
120
+ route: route.id,
121
+ message: `${route.provider}/${route.id} is not a queued route; use generate or stream`,
122
+ }),
115
123
  });
116
124
  const start = (route, request) => {
117
125
  if (route.kind !== "queued")
@@ -143,11 +151,10 @@ export const dispatch = (input) => {
143
151
  // Transport plumbing shared by every kind
144
152
  // ---------------------------------------------------------------------------
145
153
  const makeTransport = (input) => {
146
- const provider = ProviderID.make(input.provider);
147
154
  const routeHttp = input.headers === undefined ? undefined : new HttpOptions({ headers: input.headers });
148
155
  const authorize = Auth.toEffect(input.auth);
149
- const baseURL = (path) => new URL(`${ProviderShared.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`);
150
- /** `auth` is only what `Auth` added, never deployment headers. */
156
+ const baseURL = (path) => new URL(`${Endpoint.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`);
157
+ /** `auth` is only what `Auth` added or changed, never untouched deployment headers. */
151
158
  const send = Effect.fn("MediaRoute.send")(function* (call, execute) {
152
159
  const encoded = encode(call.body, call.headers);
153
160
  const url = call.url.toString();
@@ -159,15 +166,17 @@ const makeTransport = (input) => {
159
166
  headers: encoded.headers,
160
167
  });
161
168
  const response = yield* execute(encoded.apply(HttpClientRequest.make(call.method)(url).pipe(HttpClientRequest.setHeaders(headers))));
162
- return { response, auth: Object.fromEntries(Object.entries(headers).filter(([key]) => !(key in call.headers))) };
169
+ return {
170
+ response,
171
+ auth: Object.fromEntries(Object.entries(headers).filter(([key, value]) => encoded.headers[key] !== value)),
172
+ };
163
173
  });
164
174
  return {
165
- provider,
166
175
  /** Route and model overlays; `start` additionally merges the request's own `http`. */
167
176
  http: (model) => mergeHttpOptions(routeHttp, model.http),
168
177
  /** POST the protocol body to the route endpoint. */
169
178
  submit: Effect.fn("MediaRoute.submit")(function* (request, protocol, execute) {
170
- yield* rejectUnsupported(input.id, provider, request, protocol.unsupported);
179
+ yield* rejectUnsupported(input.protocol.id, input.protocol.provider, request, protocol.unsupported);
171
180
  const http = mergeHttpOptions(routeHttp, request.model.http, request.http);
172
181
  const headers = Headers.fromInput(http?.headers);
173
182
  const prepared = protocol.prepare === undefined
@@ -202,7 +211,7 @@ const encode = (body, headers) => {
202
211
  if (body === undefined)
203
212
  return { text: "", headers, apply: (request) => request };
204
213
  if (body.type === "json") {
205
- const text = ProviderShared.encodeJson(body.value);
214
+ const text = encodeJson(body.value);
206
215
  return { text, headers, apply: HttpClientRequest.bodyText(text, "application/json") };
207
216
  }
208
217
  if (body.type === "binary")
@@ -226,11 +235,13 @@ const rejectUnsupported = (route, provider, request, unsupported) => {
226
235
  });
227
236
  if (present.length === 0)
228
237
  return Effect.void;
229
- return Effect.fail(ProviderShared.unsupportedOperation({
230
- operation: `media.${present[0]}`,
231
- provider,
232
- route,
233
- message: `${provider}/${route} does not support ${present.join(", ")}`,
238
+ return Effect.fail(new AIError({
239
+ reason: new UnsupportedOperationError({
240
+ operation: `media.${present[0]}`,
241
+ provider,
242
+ route,
243
+ message: `${provider}/${route} does not support ${present.join(", ")}`,
244
+ }),
234
245
  }));
235
246
  };
236
247
  export * as MediaRoute from "./media.js";
@@ -18,8 +18,9 @@ export declare class HttpOptions extends HttpOptions_base {
18
18
  }
19
19
  export declare namespace HttpOptions {
20
20
  type Input = HttpOptions | ConstructorParameters<typeof HttpOptions>[0];
21
- /** Normalize HTTP option input into the canonical `HttpOptions` class. */
22
- const make: (input: Input) => HttpOptions;
21
+ /** Normalize HTTP option input into the canonical `HttpOptions` class; `undefined` stays `undefined`. */
22
+ function make(input: Input): HttpOptions;
23
+ function make(input: Input | undefined): HttpOptions | undefined;
23
24
  }
24
25
  export declare const mergeHttpOptions: (...items: ReadonlyArray<HttpOptions | undefined>) => HttpOptions | undefined;
25
26
  declare const GenerationOptions_base: Schema.Class<GenerationOptions, Schema.Struct<{
@@ -67,8 +68,10 @@ export declare namespace LanguageModelDefaults {
67
68
  /** Normalize selected-model request defaults without applying precedence. */
68
69
  const make: (input: Input) => LanguageModelDefaults;
69
70
  }
71
+ /** Provider-defined string enum: known values for autocomplete, any string accepted. */
72
+ export type OpenString<Known extends string> = Known | (string & {});
70
73
  export declare const ReasoningEfforts: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
71
- export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {});
74
+ export type ReasoningEffort = OpenString<(typeof ReasoningEfforts)[number]>;
72
75
  export declare const ReasoningEffort: Schema.declare<ReasoningEffort, ReasoningEffort>;
73
76
  /** Tool schema sanitizer for a model family. `none` opts out of the protocol and model-name defaults. */
74
77
  export declare const LanguageModelSanitizerCompatibility: Schema.Literals<readonly ["gemini", "moonshot", "none"]>;
@@ -39,8 +39,12 @@ export class HttpOptions extends Schema.Class("AI.HttpOptions")({
39
39
  }) {
40
40
  }
41
41
  (function (HttpOptions) {
42
- /** Normalize HTTP option input into the canonical `HttpOptions` class. */
43
- HttpOptions.make = (input) => (input instanceof HttpOptions ? input : new HttpOptions(input));
42
+ function make(input) {
43
+ if (input === undefined || input instanceof HttpOptions)
44
+ return input;
45
+ return new HttpOptions(input);
46
+ }
47
+ HttpOptions.make = make;
44
48
  })(HttpOptions || (HttpOptions = {}));
45
49
  export const mergeHttpOptions = (...items) => {
46
50
  const body = mergeJsonRecords(...items.map((item) => item?.body));
@@ -93,7 +97,7 @@ export class LanguageModelDefaults extends Schema.Class("LLM.LanguageModelDefaul
93
97
  return new LanguageModelDefaults({
94
98
  generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
95
99
  providerOptions: input.providerOptions,
96
- http: input.http === undefined ? undefined : HttpOptions.make(input.http),
100
+ http: HttpOptions.make(input.http),
97
101
  });
98
102
  };
99
103
  })(LanguageModelDefaults || (LanguageModelDefaults = {}));
package/dist/speech.d.ts CHANGED
@@ -3,7 +3,7 @@ import { Media } from "./media.js";
3
3
  import { MediaModel } from "./media-model.js";
4
4
  import { MediaRoute } from "./route/media.js";
5
5
  import type { MediaProtocol } from "./route/media-protocol.js";
6
- import { AIError, HttpOptions } from "./schema/index.js";
6
+ import { AIError, HttpOptions, type OpenString } from "./schema/index.js";
7
7
  import { Service } from "./speech-client.js";
8
8
  export type SpeechOptions = Record<string, unknown>;
9
9
  export type SpeechRoute<Options extends SpeechOptions = SpeechOptions> = MediaRoute.StreamRoute<SpeechRequestFor<Options>, SpeechEvent, SpeechResponse>;
@@ -22,7 +22,7 @@ export declare const SpeechVoice: Schema.Union<readonly [Schema.String, Schema.S
22
22
  readonly id: Schema.String;
23
23
  }>]>;
24
24
  export type SpeechVoice = Schema.Schema.Type<typeof SpeechVoice>;
25
- export type SpeechFormat = "mp3" | "wav" | "pcm" | "opus" | "aac" | "flac" | (string & {});
25
+ export type SpeechFormat = OpenString<"mp3" | "wav" | "pcm" | "opus" | "aac" | "flac">;
26
26
  /** Granularity is provider-native: characters on ElevenLabs, words on Cartesia. */
27
27
  export declare const SpeechTimestamp: Schema.Struct<{
28
28
  readonly text: Schema.String;
package/dist/speech.js CHANGED
@@ -12,7 +12,7 @@ export class SpeechModel extends MediaModel {
12
12
  static fromRoute(route, input) {
13
13
  return new SpeechModel({
14
14
  id: input.id,
15
- provider: route.provider,
15
+ provider: route.protocol.provider,
16
16
  http: input.http,
17
17
  route: composeRoute((composition) => MediaRoute.stream({ ...composition, collect: collectResponse }), route, input),
18
18
  });
@@ -102,7 +102,7 @@ export function request(input) {
102
102
  return input;
103
103
  return new SpeechRequest({
104
104
  ...input,
105
- http: input.http === undefined ? undefined : HttpOptions.make(input.http),
105
+ http: HttpOptions.make(input.http),
106
106
  });
107
107
  }
108
108
  const requestEffect = (input) => tryRequest(() => request(input));
@@ -12,7 +12,7 @@ export class TranscriptionModel extends MediaModel {
12
12
  static fromRoute(route, input) {
13
13
  return new TranscriptionModel({
14
14
  id: input.id,
15
- provider: route.provider,
15
+ provider: route.protocol.provider,
16
16
  http: input.http,
17
17
  route: composeAnyRoute(route, input, collectResponse),
18
18
  });
@@ -110,7 +110,7 @@ export function request(input) {
110
110
  return input;
111
111
  return new TranscriptionRequest({
112
112
  ...input,
113
- http: input.http === undefined ? undefined : HttpOptions.make(input.http),
113
+ http: HttpOptions.make(input.http),
114
114
  });
115
115
  }
116
116
  const requestEffect = (input) => tryRequest(() => request(input));