@opencode/ai 0.0.0-dev-20048 → 0.0.0-dev-20050

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 (51) hide show
  1. package/README.md +65 -1
  2. package/dist/generation.d.ts +13 -9
  3. package/dist/generation.js +11 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +3 -0
  6. package/dist/promise.d.ts +164 -3
  7. package/dist/promise.js +11 -1
  8. package/dist/protocols/assemblyai-transcription.d.ts +40 -0
  9. package/dist/protocols/assemblyai-transcription.js +138 -0
  10. package/dist/protocols/deepgram-speech.js +17 -15
  11. package/dist/protocols/deepgram-transcription.d.ts +25 -0
  12. package/dist/protocols/deepgram-transcription.js +129 -0
  13. package/dist/protocols/gemini.d.ts +2 -2
  14. package/dist/protocols/gemini.js +2 -6
  15. package/dist/protocols/google-images.js +2 -14
  16. package/dist/protocols/google-speech.d.ts +2 -32
  17. package/dist/protocols/google-speech.js +22 -60
  18. package/dist/protocols/google-transcription.d.ts +173 -0
  19. package/dist/protocols/google-transcription.js +138 -0
  20. package/dist/protocols/openai-images.js +4 -7
  21. package/dist/protocols/openai-transcription.d.ts +207 -0
  22. package/dist/protocols/openai-transcription.js +190 -0
  23. package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
  24. package/dist/protocols/utils/gemini-generate-content.js +65 -0
  25. package/dist/protocols/utils/media-input.d.ts +4 -0
  26. package/dist/protocols/utils/media-input.js +14 -0
  27. package/dist/providers/assemblyai.d.ts +25 -0
  28. package/dist/providers/assemblyai.js +29 -0
  29. package/dist/providers/deepgram.d.ts +5 -0
  30. package/dist/providers/deepgram.js +5 -2
  31. package/dist/providers/google.d.ts +5 -0
  32. package/dist/providers/google.js +3 -0
  33. package/dist/providers/index.d.ts +1 -0
  34. package/dist/providers/index.js +1 -0
  35. package/dist/providers/openai.d.ts +5 -0
  36. package/dist/providers/openai.js +4 -0
  37. package/dist/route/media-protocol.d.ts +15 -3
  38. package/dist/route/media-protocol.js +6 -0
  39. package/dist/route/media.d.ts +5 -2
  40. package/dist/route/media.js +62 -40
  41. package/dist/transcription-client.d.ts +28 -0
  42. package/dist/transcription-client.js +69 -0
  43. package/dist/transcription.d.ts +1504 -0
  44. package/dist/transcription.js +140 -0
  45. package/dist/utils/media-type.d.ts +1 -0
  46. package/dist/utils/media-type.js +14 -0
  47. package/dist/video-client.d.ts +1 -1
  48. package/dist/video-client.js +3 -6
  49. package/dist/video.d.ts +1 -15
  50. package/dist/video.js +2 -14
  51. package/package.json +3 -3
@@ -0,0 +1,65 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Framing } from "../../route/framing.js";
3
+ import { AIError, ContentPolicyError, ProviderID } from "../../schema/index.js";
4
+ import { ProviderShared } from "../shared.js";
5
+ import { MediaInput } from "./media-input.js";
6
+ const PROVIDER = ProviderID.make("google");
7
+ const UsageMetadata = Schema.Struct({
8
+ promptTokenCount: Schema.optional(Schema.Number),
9
+ candidatesTokenCount: Schema.optional(Schema.Number),
10
+ totalTokenCount: Schema.optional(Schema.Number),
11
+ });
12
+ export const chunk = (part) => Schema.Struct({
13
+ candidates: Schema.optional(Schema.Array(Schema.Struct({
14
+ content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(part)) })),
15
+ finishReason: Schema.optional(Schema.String),
16
+ }))),
17
+ promptFeedback: Schema.optional(Schema.Struct({
18
+ blockReason: Schema.optional(Schema.String),
19
+ blockReasonMessage: Schema.optional(Schema.String),
20
+ })),
21
+ usageMetadata: Schema.optional(UsageMetadata),
22
+ modelVersion: Schema.optional(Schema.String),
23
+ responseId: Schema.optional(Schema.String),
24
+ });
25
+ export const track = (state, chunk) => ({
26
+ ...state,
27
+ usage: chunk.usageMetadata ?? state.usage,
28
+ finishReason: chunk.candidates?.[0]?.finishReason ?? state.finishReason,
29
+ modelVersion: chunk.modelVersion ?? state.modelVersion,
30
+ responseId: chunk.responseId ?? state.responseId,
31
+ });
32
+ export const blocked = (name, chunk, frame) => {
33
+ const feedback = chunk.promptFeedback;
34
+ if (feedback?.blockReason === undefined)
35
+ return undefined;
36
+ return new AIError({
37
+ reason: new ContentPolicyError({
38
+ message: `${name} blocked the request (${feedback.blockReason})${feedback.blockReasonMessage === undefined ? "" : `: ${feedback.blockReasonMessage}`}`,
39
+ body: frame,
40
+ }),
41
+ });
42
+ };
43
+ export const usage = (usage) => usage === undefined
44
+ ? undefined
45
+ : {
46
+ type: "tokens",
47
+ input: usage.promptTokenCount,
48
+ output: usage.candidatesTokenCount,
49
+ total: ProviderShared.totalTokens(usage.promptTokenCount, usage.candidatesTokenCount, usage.totalTokenCount),
50
+ details: { google: usage },
51
+ };
52
+ export const providerMetadata = (state) => ({
53
+ google: { finishReason: state.finishReason, modelVersion: state.modelVersion, responseId: state.responseId },
54
+ });
55
+ export const path = (model, mode) => mode === "stream" ? `/models/${model}:streamGenerateContent?alt=sse` : `/models/${model}:generateContent`;
56
+ // `generateContent` answers with one document shaped exactly like a streamed chunk, so it is a single frame.
57
+ export const frames = (bytes, mode) => mode === "stream" ? Framing.sse.frame(bytes) : Framing.document.frame(bytes);
58
+ // Gemini does not fetch public URLs; inline payloads and Gemini Files references are the accepted inputs.
59
+ export const mediaPart = (route, asset) => {
60
+ const fileUri = MediaInput.refID(asset, PROVIDER);
61
+ if (fileUri !== undefined)
62
+ return Effect.succeed({ fileData: { mimeType: asset.mediaType, fileUri } });
63
+ return ProviderShared.requireInlineMedia(route, asset).pipe(Effect.map((media) => ({ inlineData: { mimeType: media.mime, data: media.base64 } })));
64
+ };
65
+ export * as GeminiGenerateContent from "./gemini-generate-content.js";
@@ -1,8 +1,12 @@
1
1
  import { Effect } from "effect";
2
2
  import { Media } from "../../media.js";
3
+ import type { MediaProtocol } from "../../route/media-protocol.js";
3
4
  import type { AIError, ProviderID } from "../../schema/index.js";
4
5
  /** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
5
6
  export declare const inlineBytes: (route: string, asset: Media.Asset) => Effect.Effect<Uint8Array, AIError>;
7
+ /** Copied because `BlobPart` requires a plain `ArrayBuffer`. */
8
+ export declare const blob: (data: Uint8Array, mediaType: string) => Blob;
9
+ export declare const query: (route: string, values: Record<string, unknown>) => Effect.Effect<MediaProtocol.Query, AIError>;
6
10
  /** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
7
11
  export declare const refID: (asset: Media.Asset, provider: ProviderID) => string | undefined;
8
12
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
@@ -10,6 +10,20 @@ export const inlineBytes = (route, asset) => {
10
10
  return Effect.fail(ProviderShared.inlineRequired(route, asset));
11
11
  return Effect.fromResult(Encoding.decodeBase64(inline.base64)).pipe(Effect.mapError((cause) => ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)));
12
12
  };
13
+ /** Copied because `BlobPart` requires a plain `ArrayBuffer`. */
14
+ export const blob = (data, mediaType) => {
15
+ const buffer = new ArrayBuffer(data.byteLength);
16
+ new Uint8Array(buffer).set(data);
17
+ return new Blob([buffer], { type: mediaType });
18
+ };
19
+ const isScalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
20
+ export const query = (route, values) => {
21
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
22
+ const invalid = entries.find(([, value]) => !isScalar(value) && !(Array.isArray(value) && value.every(isScalar)));
23
+ if (invalid !== undefined)
24
+ return Effect.fail(ProviderShared.invalidRequest(`${route} cannot send "${invalid[0]}" as a query parameter`));
25
+ return Effect.succeed(Object.fromEntries(entries.map(([key, value]) => [key, Array.isArray(value) ? value.map(String) : String(value)])));
26
+ };
13
27
  /** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
14
28
  export const refID = (asset, provider) => asset.source.type === "ref" && asset.source.provider === provider ? asset.source.id : undefined;
15
29
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
@@ -0,0 +1,25 @@
1
+ import type { ProviderAuthOption } from "../route/auth-options.js";
2
+ import { HttpOptions, type ModelID } from "../schema/index.js";
3
+ export type { AssemblyAITranscriptionOptions } from "../protocols/assemblyai-transcription.js";
4
+ export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
5
+ export type Config = ProviderAuthOption<"optional"> & {
6
+ /** `https://api.eu.assemblyai.com` for the EU region. */
7
+ readonly baseURL?: string;
8
+ readonly headers?: Record<string, string>;
9
+ readonly http?: HttpOptions.Input;
10
+ };
11
+ export declare const configure: (input?: Config) => {
12
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
13
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/assemblyai-transcription.js").AssemblyAITranscriptionOptions>;
14
+ configure: (input?: Config) => /*elided*/ any;
15
+ };
16
+ export declare const provider: {
17
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
18
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/assemblyai-transcription.js").AssemblyAITranscriptionOptions>;
19
+ configure: (input?: Config) => {
20
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
21
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/assemblyai-transcription.js").AssemblyAITranscriptionOptions>;
22
+ configure: /*elided*/ any;
23
+ };
24
+ };
25
+ export declare const transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/assemblyai-transcription.js").AssemblyAITranscriptionOptions>;
@@ -0,0 +1,29 @@
1
+ import { Auth } from "../route/auth.js";
2
+ import { HttpOptions, ProviderID } from "../schema/index.js";
3
+ import { AssemblyAITranscription, DEFAULT_BASE_URL } from "../protocols/assemblyai-transcription.js";
4
+ export const id = ProviderID.make("assemblyai");
5
+ const baseURL = DEFAULT_BASE_URL;
6
+ // The key is the whole `authorization` value, without a scheme.
7
+ const auth = (options) => {
8
+ if ("auth" in options && options.auth)
9
+ return options.auth;
10
+ return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
11
+ .orElse(Auth.config("ASSEMBLYAI_API_KEY"))
12
+ .pipe(Auth.header("authorization"));
13
+ };
14
+ export const configure = (input = {}) => {
15
+ const transcription = (modelID) => AssemblyAITranscription.model({
16
+ id: modelID,
17
+ auth: auth(input),
18
+ baseURL: input.baseURL ?? baseURL,
19
+ headers: input.headers,
20
+ http: input.http === undefined ? undefined : HttpOptions.make(input.http),
21
+ });
22
+ return {
23
+ id,
24
+ transcription,
25
+ configure,
26
+ };
27
+ };
28
+ export const provider = configure();
29
+ export const transcription = provider.transcription;
@@ -1,6 +1,7 @@
1
1
  import type { ProviderAuthOption } from "../route/auth-options.js";
2
2
  import { HttpOptions, type ModelID } from "../schema/index.js";
3
3
  export type { DeepgramEncoding, DeepgramSpeechOptions } from "../protocols/deepgram-speech.js";
4
+ export type { DeepgramTranscriptionOptions } from "../protocols/deepgram-transcription.js";
4
5
  export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
5
6
  export type Config = ProviderAuthOption<"optional"> & {
6
7
  readonly baseURL?: string;
@@ -10,15 +11,19 @@ export type Config = ProviderAuthOption<"optional"> & {
10
11
  export declare const configure: (input?: Config) => {
11
12
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
12
13
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/deepgram-speech.js").DeepgramSpeechOptions>;
14
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/deepgram-transcription.js").DeepgramTranscriptionOptions>;
13
15
  configure: (input?: Config) => /*elided*/ any;
14
16
  };
15
17
  export declare const provider: {
16
18
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
17
19
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/deepgram-speech.js").DeepgramSpeechOptions>;
20
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/deepgram-transcription.js").DeepgramTranscriptionOptions>;
18
21
  configure: (input?: Config) => {
19
22
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
20
23
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/deepgram-speech.js").DeepgramSpeechOptions>;
24
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/deepgram-transcription.js").DeepgramTranscriptionOptions>;
21
25
  configure: /*elided*/ any;
22
26
  };
23
27
  };
24
28
  export declare const speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/deepgram-speech.js").DeepgramSpeechOptions>;
29
+ export declare const transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/deepgram-transcription.js").DeepgramTranscriptionOptions>;
@@ -1,6 +1,7 @@
1
1
  import { Auth } from "../route/auth.js";
2
2
  import { HttpOptions, ProviderID } from "../schema/index.js";
3
3
  import { DEFAULT_BASE_URL, DeepgramSpeech } from "../protocols/deepgram-speech.js";
4
+ import { DeepgramTranscription } from "../protocols/deepgram-transcription.js";
4
5
  export const id = ProviderID.make("deepgram");
5
6
  const baseURL = DEFAULT_BASE_URL;
6
7
  const auth = (options) => {
@@ -11,7 +12,7 @@ const auth = (options) => {
11
12
  .pipe(Auth.scheme("Token"));
12
13
  };
13
14
  export const configure = (input = {}) => {
14
- const speech = (modelID) => DeepgramSpeech.model({
15
+ const media = (modelID) => ({
15
16
  id: modelID,
16
17
  auth: auth(input),
17
18
  baseURL: input.baseURL ?? baseURL,
@@ -20,9 +21,11 @@ export const configure = (input = {}) => {
20
21
  });
21
22
  return {
22
23
  id,
23
- speech,
24
+ speech: (modelID) => DeepgramSpeech.model(media(modelID)),
25
+ transcription: (modelID) => DeepgramTranscription.model(media(modelID)),
24
26
  configure,
25
27
  };
26
28
  };
27
29
  export const provider = configure();
28
30
  export const speech = provider.speech;
31
+ export const transcription = provider.transcription;
@@ -5,6 +5,7 @@ import { type ModelID } from "../schema/index.js";
5
5
  import { Gemini } from "../protocols/gemini.js";
6
6
  export type { GoogleImageOptions } from "../protocols/google-images.js";
7
7
  export type { GoogleSpeechOptions } from "../protocols/google-speech.js";
8
+ export type { GoogleTranscriptionOptions } from "../protocols/google-transcription.js";
8
9
  export type { GoogleVideoOptions } from "../protocols/google-video.js";
9
10
  export type GeminiOptionsInput = Gemini.OptionsInput;
10
11
  export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput;
@@ -108,6 +109,7 @@ export declare const configure: (input?: Config) => {
108
109
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/google-images.js").GoogleImageOptions>;
109
110
  video: (modelID: string | ModelID) => import("../video.js").VideoModel<import("../protocols/google-video.js").GoogleVideoOptions>;
110
111
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/google-speech.js").GoogleSpeechOptions>;
112
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/google-transcription.js").GoogleTranscriptionOptions>;
111
113
  configure: (input?: Config) => /*elided*/ any;
112
114
  };
113
115
  export declare const provider: {
@@ -116,12 +118,14 @@ export declare const provider: {
116
118
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/google-images.js").GoogleImageOptions>;
117
119
  video: (modelID: string | ModelID) => import("../video.js").VideoModel<import("../protocols/google-video.js").GoogleVideoOptions>;
118
120
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/google-speech.js").GoogleSpeechOptions>;
121
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/google-transcription.js").GoogleTranscriptionOptions>;
119
122
  configure: (input?: Config) => {
120
123
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
121
124
  model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<Gemini.OptionsInput, import("../route/client.js").CompactionOperations | undefined>;
122
125
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/google-images.js").GoogleImageOptions>;
123
126
  video: (modelID: string | ModelID) => import("../video.js").VideoModel<import("../protocols/google-video.js").GoogleVideoOptions>;
124
127
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/google-speech.js").GoogleSpeechOptions>;
128
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/google-transcription.js").GoogleTranscriptionOptions>;
125
129
  configure: /*elided*/ any;
126
130
  };
127
131
  };
@@ -129,3 +133,4 @@ export declare const model: ProviderPackage.Definition<Settings, Gemini.Provider
129
133
  export declare const image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/google-images.js").GoogleImageOptions>;
130
134
  export declare const video: (modelID: string | ModelID) => import("../video.js").VideoModel<import("../protocols/google-video.js").GoogleVideoOptions>;
131
135
  export declare const speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/google-speech.js").GoogleSpeechOptions>;
136
+ export declare const transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/google-transcription.js").GoogleTranscriptionOptions>;
@@ -3,6 +3,7 @@ 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
5
  import { GoogleSpeech } from "../protocols/google-speech.js";
6
+ import { GoogleTranscription } from "../protocols/google-transcription.js";
6
7
  import { GoogleVideo } from "../protocols/google-video.js";
7
8
  export const id = ProviderID.make("google");
8
9
  export const routes = [Gemini.route];
@@ -32,6 +33,7 @@ export const configure = (input = {}) => {
32
33
  image: (modelID) => GoogleImages.model(media(modelID)),
33
34
  video: (modelID) => GoogleVideo.model(media(modelID)),
34
35
  speech: (modelID) => GoogleSpeech.model(media(modelID)),
36
+ transcription: (modelID) => GoogleTranscription.model(media(modelID)),
35
37
  configure,
36
38
  };
37
39
  };
@@ -46,3 +48,4 @@ export const model = (modelID, { apiKey, baseURL, body, headers, ...providerOpti
46
48
  export const image = provider.image;
47
49
  export const video = provider.video;
48
50
  export const speech = provider.speech;
51
+ export const transcription = provider.transcription;
@@ -3,6 +3,7 @@ export * as Anthropic from "./anthropic.js";
3
3
  export * as AnthropicCompatible from "./anthropic-compatible.js";
4
4
  export * as AmazonBedrock from "./amazon-bedrock.js";
5
5
  export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js";
6
+ export * as AssemblyAI from "./assemblyai.js";
6
7
  export * as Azure from "./azure.js";
7
8
  export * as Baseten from "./baseten.js";
8
9
  export * as Cartesia from "./cartesia.js";
@@ -3,6 +3,7 @@ export * as Anthropic from "./anthropic.js";
3
3
  export * as AnthropicCompatible from "./anthropic-compatible.js";
4
4
  export * as AmazonBedrock from "./amazon-bedrock.js";
5
5
  export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js";
6
+ export * as AssemblyAI from "./assemblyai.js";
6
7
  export * as Azure from "./azure.js";
7
8
  export * as Baseten from "./baseten.js";
8
9
  export * as Cartesia from "./cartesia.js";
@@ -8,6 +8,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
10
  export type { OpenAISpeechOptions } from "../protocols/openai-speech.js";
11
+ export type { OpenAITranscriptionOptions } from "../protocols/openai-transcription.js";
11
12
  export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
12
13
  export declare const routes: (Route<{
13
14
  readonly model: string;
@@ -374,6 +375,7 @@ export declare const configure: (input?: Config) => {
374
375
  chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
375
376
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
376
377
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
378
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/openai-transcription.js").OpenAITranscriptionOptions>;
377
379
  configure: (input?: Config) => /*elided*/ any;
378
380
  };
379
381
  export declare const provider: {
@@ -389,6 +391,7 @@ export declare const provider: {
389
391
  chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
390
392
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
391
393
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
394
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/openai-transcription.js").OpenAITranscriptionOptions>;
392
395
  configure: (input?: Config) => {
393
396
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
394
397
  model: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, {
@@ -402,6 +405,7 @@ export declare const provider: {
402
405
  chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
403
406
  image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
404
407
  speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
408
+ transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/openai-transcription.js").OpenAITranscriptionOptions>;
405
409
  configure: /*elided*/ any;
406
410
  };
407
411
  };
@@ -414,3 +418,4 @@ export declare const responses: (id: string | ModelID) => import("../schema/opti
414
418
  export declare const chat: (id: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, CompactionOperations | undefined>;
415
419
  export declare const image: (modelID: string | ModelID) => import("../image.js").ImageModel<import("../protocols/openai-images.js").OpenAIImageOptions>;
416
420
  export declare const speech: (modelID: string | ModelID) => import("../speech.js").SpeechModel<import("../protocols/openai-speech.js").OpenAISpeechOptions>;
421
+ export declare const transcription: (modelID: string | ModelID) => import("../transcription.js").TranscriptionModel<import("../protocols/openai-transcription.js").OpenAITranscriptionOptions>;
@@ -5,6 +5,7 @@ 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
7
  import { OpenAISpeech } from "../protocols/openai-speech.js";
8
+ import { OpenAITranscription } from "../protocols/openai-transcription.js";
8
9
  export const id = ProviderID.make("openai");
9
10
  export const routes = [OpenAIResponses.route, OpenAIChat.route];
10
11
  export const imageGeneration = (options = {}) => ToolDefinition.make({
@@ -54,6 +55,7 @@ export const configure = (input = {}) => {
54
55
  });
55
56
  const image = (modelID) => OpenAIImages.model(media(modelID));
56
57
  const speech = (modelID) => OpenAISpeech.model(media(modelID));
58
+ const transcription = (modelID) => OpenAITranscription.model(media(modelID));
57
59
  return {
58
60
  id,
59
61
  model: responses,
@@ -61,6 +63,7 @@ export const configure = (input = {}) => {
61
63
  chat,
62
64
  image,
63
65
  speech,
66
+ transcription,
64
67
  configure,
65
68
  };
66
69
  };
@@ -88,3 +91,4 @@ export const responses = provider.responses;
88
91
  export const chat = provider.chat;
89
92
  export const image = provider.image;
90
93
  export const speech = provider.speech;
94
+ export const transcription = provider.transcription;
@@ -4,17 +4,28 @@ 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
+ /** Array values become repeated parameters (`keyterm=a&keyterm=b`). */
8
+ export type Query = Readonly<Record<string, string | ReadonlyArray<string>>>;
9
+ /** `query` is appended to the endpoint URL before the route and caller `http.query` overlays. */
8
10
  export type Body = {
9
11
  readonly type: "json";
10
12
  readonly value: Record<string, unknown>;
11
- readonly query?: Record<string, string>;
13
+ readonly query?: Query;
12
14
  } | {
13
15
  readonly type: "multipart";
14
16
  readonly value: FormData;
17
+ } | {
18
+ readonly type: "binary";
19
+ readonly value: Uint8Array;
20
+ readonly contentType: string;
21
+ readonly query?: Query;
15
22
  };
16
- export declare const json: (value: Record<string, unknown>, query?: Record<string, string>) => Body;
23
+ export declare const json: (value: Record<string, unknown>, query?: Query) => Body;
17
24
  export declare const multipart: (value: FormData) => Body;
25
+ export declare const binary: (value: Uint8Array, contentType: string, query?: Query) => Body;
26
+ export type Send = (path: string, body: Body) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>;
27
+ /** Runs after unsupported-field rejection and before `body.from`, for providers that need an upload first. */
28
+ export type Prepare<Request> = (request: Request, send: Send) => Effect.Effect<Request, AIError>;
18
29
  export interface DecodeContext<Request> {
19
30
  readonly request: Request;
20
31
  readonly body: Body;
@@ -63,6 +74,7 @@ export interface Queued<Request, Response, Token> {
63
74
  /** Serializable handle. `Generation.token` carries the encoded form so it can be persisted and resumed elsewhere. */
64
75
  readonly token: Schema.Codec<Token, unknown>;
65
76
  readonly start: {
77
+ readonly prepare?: Prepare<Request>;
66
78
  readonly body: {
67
79
  readonly from: (request: Request) => Effect.Effect<Body, AIError>;
68
80
  };
@@ -8,6 +8,12 @@ export const json = (value, query) => ({
8
8
  query,
9
9
  });
10
10
  export const multipart = (value) => ({ type: "multipart", value });
11
+ export const binary = (value, contentType, query) => ({
12
+ type: "binary",
13
+ value,
14
+ contentType,
15
+ query,
16
+ });
11
17
  export const inline = (input) => ({
12
18
  kind: "inline",
13
19
  ...input,
@@ -25,6 +25,7 @@ export interface ModelInput {
25
25
  }
26
26
  /** One request, one response. */
27
27
  export interface Route<Request extends MediaRequest, Response> {
28
+ readonly kind: "inline";
28
29
  readonly id: string;
29
30
  readonly provider: ProviderID;
30
31
  readonly protocol: string;
@@ -32,6 +33,7 @@ export interface Route<Request extends MediaRequest, Response> {
32
33
  }
33
34
  /** Submit, then poll through the returned `Generation`. */
34
35
  export interface QueuedRoute<Request extends MediaRequest, Response> {
36
+ readonly kind: "queued";
35
37
  readonly id: string;
36
38
  readonly provider: ProviderID;
37
39
  readonly protocol: string;
@@ -41,6 +43,7 @@ export interface QueuedRoute<Request extends MediaRequest, Response> {
41
43
  }
42
44
  /** One request whose response parses into events; `generate` runs the same stream and collects it. */
43
45
  export interface StreamRoute<Request extends MediaRequest, Event, Response> {
46
+ readonly kind: "stream";
44
47
  readonly id: string;
45
48
  readonly provider: ProviderID;
46
49
  readonly protocol: string;
@@ -68,8 +71,8 @@ export interface StreamInput<Request extends MediaRequest, Event, Response, Fram
68
71
  /**
69
72
  * Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
70
73
  * plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field
71
- * rejection, URL and query rendering, auth headers, JSON vs multipart encoding, and handing responses back to the
72
- * protocol.
74
+ * rejection, URL and query rendering, auth headers, JSON, multipart, or binary encoding, and handing responses back
75
+ * to the protocol.
73
76
  */
74
77
  export declare const inline: <Request extends MediaRequest, Response>(input: InlineInput<Request, Response>) => Route<Request, Response>;
75
78
  /**
@@ -11,12 +11,13 @@ import { sanitizeSurrogates } from "../utils/sanitize.js";
11
11
  /**
12
12
  * Compose an inline media protocol with an endpoint and auth into a runnable route. The route owns the transport
13
13
  * plumbing every media protocol would otherwise duplicate: option merging, surrogate sanitizing, unsupported-field
14
- * rejection, URL and query rendering, auth headers, JSON vs multipart encoding, and handing responses back to the
15
- * protocol.
14
+ * rejection, URL and query rendering, auth headers, JSON, multipart, or binary encoding, and handing responses back
15
+ * to the protocol.
16
16
  */
17
17
  export const inline = (input) => {
18
18
  const transport = makeTransport(input);
19
19
  return {
20
+ kind: "inline",
20
21
  id: input.id,
21
22
  provider: transport.provider,
22
23
  protocol: input.protocol.id,
@@ -52,7 +53,7 @@ export const queued = (input) => {
52
53
  return route;
53
54
  };
54
55
  const start = Effect.fn("MediaRoute.start")(function* (request, execute) {
55
- const submitted = yield* transport.submit(request, { unsupported: protocol.unsupported, from: protocol.start.body.from }, execute);
56
+ const submitted = yield* transport.submit(request, { unsupported: protocol.unsupported, prepare: protocol.start.prepare, from: protocol.start.body.from }, execute);
56
57
  const started = yield* protocol.start.decode(submitted.response, submitted.context);
57
58
  const route = generationRoute(started.token, submitted.context.request.http, execute);
58
59
  return new Generation(route, encodeToken(started.token), started.snapshot);
@@ -67,7 +68,7 @@ export const queued = (input) => {
67
68
  const route = generationRoute(token, transport.http(model), execute);
68
69
  return new Generation(route, encodeToken(token), yield* route.status);
69
70
  });
70
- return { id: input.id, provider: transport.provider, protocol: protocol.id, start, resume };
71
+ return { kind: "queued", id: input.id, provider: transport.provider, protocol: protocol.id, start, resume };
71
72
  };
72
73
  /** Compose a streaming media protocol; `generate` runs the same stream in `generate` mode and folds it with `collect`. */
73
74
  export const stream = (input) => {
@@ -95,6 +96,7 @@ export const stream = (input) => {
95
96
  });
96
97
  }));
97
98
  return {
99
+ kind: "stream",
98
100
  id: input.id,
99
101
  provider: transport.provider,
100
102
  protocol: protocol.id,
@@ -109,11 +111,21 @@ const makeTransport = (input) => {
109
111
  const provider = ProviderID.make(input.provider);
110
112
  const routeHttp = input.headers === undefined ? undefined : new HttpOptions({ headers: input.headers });
111
113
  const authorize = Auth.toEffect(input.auth);
112
- const withQuery = (url, query) => {
113
- for (const [key, value] of Object.entries(query ?? {}))
114
- url.searchParams.set(key, value);
115
- return url;
116
- };
114
+ const baseURL = (path) => new URL(`${ProviderShared.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`);
115
+ /** `auth` is only what `Auth` added, never deployment headers. */
116
+ const send = Effect.fn("MediaRoute.send")(function* (call, execute) {
117
+ const encoded = encode(call.body, call.headers);
118
+ const url = call.url.toString();
119
+ const headers = yield* authorize({
120
+ request: call.request,
121
+ method: call.method,
122
+ url,
123
+ body: encoded.text,
124
+ headers: encoded.headers,
125
+ });
126
+ const response = yield* execute(encoded.apply(HttpClientRequest.make(call.method)(url).pipe(HttpClientRequest.setHeaders(headers))));
127
+ return { response, auth: Object.fromEntries(Object.entries(headers).filter(([key]) => !(key in call.headers))) };
128
+ });
117
129
  return {
118
130
  provider,
119
131
  /** Route and model overlays; `start` additionally merges the request's own `http`. */
@@ -122,43 +134,53 @@ const makeTransport = (input) => {
122
134
  submit: Effect.fn("MediaRoute.submit")(function* (request, protocol, execute) {
123
135
  yield* rejectUnsupported(input.id, provider, request, protocol.unsupported);
124
136
  const http = mergeHttpOptions(routeHttp, request.model.http, request.http);
137
+ const headers = Headers.fromInput(http?.headers);
138
+ const prepared = protocol.prepare === undefined
139
+ ? request
140
+ : yield* protocol.prepare(request, (path, body) => send({ method: "POST", url: baseURL(path), headers, request, body }, execute).pipe(Effect.map((sent) => sent.response)));
125
141
  // Sanitize after merging so model-level overlays are covered; the model value is restored, not sanitized.
126
- const resolved = { ...sanitizeSurrogates({ ...request, http }), model: request.model };
142
+ const resolved = { ...sanitizeSurrogates({ ...prepared, http }), model: request.model };
127
143
  const body = yield* protocol.from(resolved);
128
- const url = withQuery(withQuery(Endpoint.render(input.endpoint, { request: resolved, body }), body.type === "json" ? body.query : undefined), http?.query);
129
- const encoded = body.type === "json" ? ProviderShared.encodeJson(body.value) : "[multipart/form-data]";
130
- const baseHeaders = Headers.fromInput(http?.headers);
131
- const headers = yield* authorize({
132
- request: resolved,
133
- method: "POST",
134
- url: url.toString(),
135
- body: encoded,
136
- // The HTTP client sets the multipart boundary; a caller-supplied content-type would corrupt it.
137
- headers: body.type === "multipart" ? Headers.remove(baseHeaders, "content-type") : baseHeaders,
138
- });
139
- const transport = HttpClientRequest.post(url.toString()).pipe(HttpClientRequest.setHeaders(headers), body.type === "json"
140
- ? HttpClientRequest.bodyText(encoded, "application/json")
141
- : HttpClientRequest.bodyFormData(body.value));
142
- const response = yield* execute(transport);
143
- return { response, context: { request: resolved, body } };
144
+ const url = withQuery(withQuery(Endpoint.render(input.endpoint, { request: resolved, body }), body.type === "multipart" ? undefined : body.query), http?.query);
145
+ const sent = yield* send({ method: "POST", url, headers, request: resolved, body }, execute);
146
+ return { response: sent.response, context: { request: resolved, body } };
144
147
  }),
145
- /**
146
- * Bodiless follow-up call (status, result, cancel) with the same auth and headers as `submit`. `auth` is only
147
- * what `Auth` added, so protocols can hand download credentials to output assets without deployment headers.
148
- */
149
- call: Effect.fn("MediaRoute.call")(function* (method, path, http, execute) {
148
+ /** Bodiless follow-up call (status, result, cancel) with the same auth and headers as `submit`. */
149
+ call: (method, path, http, execute) => {
150
150
  // Provider-issued absolute URLs (fal `status_url`) are used as-is; everything else resolves against the base.
151
- const url = withQuery(/^https?:\/\//.test(path)
152
- ? new URL(path)
153
- : new URL(`${ProviderShared.trimBaseUrl(input.endpoint.baseURL ?? "")}${path}`), http?.query);
151
+ const url = withQuery(/^https?:\/\//.test(path) ? new URL(path) : baseURL(path), http?.query);
154
152
  for (const [key, value] of Object.entries(input.endpoint.query ?? {}))
155
153
  url.searchParams.set(key, value);
156
- const base = Headers.fromInput(http?.headers);
157
- const headers = yield* authorize({ request: { http }, method, url: url.toString(), body: "", headers: base });
158
- const response = yield* execute(HttpClientRequest.make(method)(url.toString()).pipe(HttpClientRequest.setHeaders(headers)));
159
- const auth = Object.fromEntries(Object.entries(headers).filter(([key]) => !(key in base)));
160
- return { response, auth };
161
- }),
154
+ return send({ method, url, headers: Headers.fromInput(http?.headers), request: { http } }, execute);
155
+ },
156
+ };
157
+ };
158
+ const withQuery = (url, query) => {
159
+ for (const [key, value] of Object.entries(query ?? {})) {
160
+ url.searchParams.delete(key);
161
+ for (const item of typeof value === "string" ? [value] : value)
162
+ url.searchParams.append(key, item);
163
+ }
164
+ return url;
165
+ };
166
+ const encode = (body, headers) => {
167
+ if (body === undefined)
168
+ return { text: "", headers, apply: (request) => request };
169
+ if (body.type === "json") {
170
+ const text = ProviderShared.encodeJson(body.value);
171
+ return { text, headers, apply: HttpClientRequest.bodyText(text, "application/json") };
172
+ }
173
+ if (body.type === "binary")
174
+ return {
175
+ text: `[${body.contentType}]`,
176
+ headers,
177
+ apply: HttpClientRequest.bodyUint8Array(body.value, body.contentType),
178
+ };
179
+ return {
180
+ text: "[multipart/form-data]",
181
+ // The HTTP client sets the multipart boundary; a caller-supplied content-type would corrupt it.
182
+ headers: Headers.remove(headers, "content-type"),
183
+ apply: HttpClientRequest.bodyFormData(body.value),
162
184
  };
163
185
  };
164
186
  /** Common fields are never silently dropped: a present field the protocol declared unsupported fails typed. */