@opencode/ai 0.0.0-dev-20115 → 0.0.0-dev-20116

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.
@@ -2,8 +2,9 @@ import { Context, Effect, Layer, Stream } from "effect";
2
2
  import { RequestExecutor } from "./route/executor.js";
3
3
  import { MediaRoute } from "./route/media.js";
4
4
  import { responseEvents, } from "./image.js";
5
- export class Service extends Context.Service()("@opencode/ImageClient") {
5
+ export class ImageClientService extends Context.Service()("@opencode/ImageClient") {
6
6
  }
7
+ export const Service = ImageClientService;
7
8
  export const generate = (request, options) => Effect.gen(function* () {
8
9
  const client = yield* Service;
9
10
  return yield* client.generate(request, options);
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
+ export { AIClient } from "./ai-client.js";
1
2
  export { LLMClient } from "./route/client.js";
2
3
  export { ImageClient } from "./image-client.js";
3
4
  export { Auth } from "./route/auth.js";
4
5
  export { Provider } from "./provider.js";
5
6
  export { ProviderPackage } from "./provider-package.js";
6
7
  export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js";
7
- export type { RouteLanguageModelInput, RouteRoutedLanguageModelInput, Interface as LLMClientShape, Service as LLMClientService, } from "./route/client.js";
8
+ export type { RouteLanguageModelInput, RouteRoutedLanguageModelInput, Interface as LLMClientShape, LLMClientService, } from "./route/client.js";
8
9
  export * from "./schema/index.js";
9
10
  export { ImageAspectRatio, ImageEvent, ImageModel, ImageModelSchema, ImageRequest, ImageResponse, ImageSize, } from "./image.js";
10
11
  export type { ImageFormat, ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute, } from "./image.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { AIClient } from "./ai-client.js";
1
2
  export { LLMClient } from "./route/client.js";
2
3
  export { ImageClient } from "./image-client.js";
3
4
  export { Auth } from "./route/auth.js";
package/dist/media.js CHANGED
@@ -5,7 +5,7 @@ import { ProviderID } from "./schema/ids.js";
5
5
  import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js";
6
6
  import { ProviderMetadata } from "./schema/options.js";
7
7
  import { Service } from "./route/executor-service.js";
8
- import { detectMediaType, extensionMediaType } from "./utils/media-type.js";
8
+ import { detectMediaType, fileMediaType } from "./utils/media-type.js";
9
9
  export { detectMediaType } from "./utils/media-type.js";
10
10
  const OCTET_STREAM = "application/octet-stream";
11
11
  // ---------------------------------------------------------------------------
@@ -215,7 +215,7 @@ export const file = (path, options) => Effect.gen(function* () {
215
215
  const data = yield* fs
216
216
  .readFile(path)
217
217
  .pipe(Effect.mapError((cause) => invalid(`Failed to read media file ${path}`, cause)));
218
- return bytes(data, detectMediaType(data) ?? extensionMediaType(path), options);
218
+ return bytes(data, fileMediaType(data, path), options);
219
219
  });
220
220
  /** Materialize an asset and write its bytes through `FileSystem`. */
221
221
  export const write = (asset, path) => Effect.gen(function* () {
package/dist/promise.d.ts CHANGED
@@ -1,40 +1,53 @@
1
1
  import { Effect, Layer } from "effect";
2
- import type { AwaitOptions, Snapshot } from "./generation.js";
2
+ import { AIClient } from "./ai-client.js";
3
+ import type { AwaitOptions, Event, Snapshot } from "./generation.js";
3
4
  import { ImageModel, ImageRequest, type ImageOptions, type ImageRequestInput } from "./image.js";
4
- import { ImageClient } from "./image-client.js";
5
- import { LLMClient } from "./route/client.js";
5
+ import { Media } from "./media.js";
6
6
  import { RequestExecutor } from "./route/executor.js";
7
7
  import { LanguageModel, LLMRequest } from "./schema/index.js";
8
8
  import type { RequestInput } from "./llm.js";
9
9
  import { SpeechModel, SpeechRequest, type SpeechRequestInput } from "./speech.js";
10
- import { SpeechClient } from "./speech-client.js";
11
10
  import { TranscriptionModel, TranscriptionRequest, type TranscriptionOptions, type TranscriptionRequestInput } from "./transcription.js";
12
- import { TranscriptionClient } from "./transcription-client.js";
13
11
  import { VideoModel, VideoRequest, type VideoOptions, type VideoRequestInput } from "./video.js";
14
- import { VideoClient } from "./video-client.js";
15
12
  /**
16
13
  * Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, video, speech,
17
14
  * and transcription clients over a request executor; every method runs the corresponding Effect API and rethrows
18
- * `AIError` unchanged.
15
+ * `AIError` unchanged. `file` and `write` load `node:fs/promises` on first use, so importing this module does not.
19
16
  */
20
17
  export interface Options {
21
- /** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or middleware here. */
18
+ /** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or `RequestExecutor.middleware(fn)` here. */
22
19
  readonly layer?: Layer.Layer<RequestExecutor.Service>;
23
20
  }
24
21
  export interface RunOptions {
25
22
  readonly signal?: AbortSignal;
26
23
  }
27
- export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | Layer.Success<typeof VideoClient.layer> | Layer.Success<typeof SpeechClient.layer> | Layer.Success<typeof TranscriptionClient.layer> | RequestExecutor.Service;
28
- /** Promise view of a `Generation`: its snapshot plus `await`, `refresh`, and `cancel` returning promises. */
24
+ export type Services = AIClient.Services;
25
+ /**
26
+ * Promise view of a `Generation`. Its fields are a snapshot taken when the handle was created; `refresh()` resolves to a
27
+ * new handle rather than updating this one.
28
+ */
29
29
  export type GenerationHandle<Response> = Snapshot & {
30
30
  /** Serializable JSON; pass it back to `resume` from another process. */
31
31
  readonly token: unknown;
32
32
  readonly await: (options?: AwaitOptions & RunOptions) => Promise<Response>;
33
+ /** Status observations until the first terminal one, polling like `await`; abort ends iteration without throwing. */
34
+ readonly events: (options?: AwaitOptions & RunOptions) => AsyncIterable<Event>;
35
+ /** The result without polling; fails when the generation has not completed. */
36
+ readonly result: (options?: RunOptions) => Promise<Response>;
33
37
  readonly refresh: (options?: RunOptions) => Promise<GenerationHandle<Response>>;
34
38
  readonly cancel: (options?: RunOptions) => Promise<void>;
35
39
  };
36
40
  export declare const make: (options?: Options) => {
37
41
  run: <A, E>(effect: Effect.Effect<A, E, Services>, options?: RunOptions) => Promise<A>;
42
+ /** Decoded asset bytes, downloading `url` sources through the executor. */
43
+ bytes: (asset: Media.Asset, options?: RunOptions) => Promise<Uint8Array<ArrayBufferLike>>;
44
+ base64: (asset: Media.Asset, options?: RunOptions) => Promise<string>;
45
+ /** Pull a `url` asset into owned bytes before the provider URL expires. */
46
+ materialize: (asset: Media.Asset, options?: RunOptions) => Promise<Media.Asset>;
47
+ /** Read a file into an asset like `Media.file`: sniffed media type, then the extension's. */
48
+ file: (path: string, options?: Media.AssetOptions & RunOptions) => Promise<Media.Asset>;
49
+ /** Write an asset's bytes like `Media.write`, downloading `url` sources through the executor. */
50
+ write: (asset: Media.Asset, path: string, options?: RunOptions) => Promise<void>;
38
51
  llm: {
39
52
  request: <const SelectedLanguageModel extends LanguageModel>(input: RequestInput<SelectedLanguageModel>) => LLMRequest<SelectedLanguageModel>;
40
53
  generate: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) => Promise<import("./index.js").LLMResponse>;
@@ -209,7 +222,7 @@ export declare const make: (options?: Options) => {
209
222
  readonly namespace?: string | undefined;
210
223
  } | {
211
224
  readonly type: "media";
212
- readonly media: import("./media.js").Asset;
225
+ readonly media: Media.Asset;
213
226
  readonly providerMetadata?: {
214
227
  readonly [x: string]: {
215
228
  readonly [x: string]: unknown;
@@ -265,11 +278,11 @@ export declare const make: (options?: Options) => {
265
278
  } | {
266
279
  readonly type: "image-partial";
267
280
  readonly index: number;
268
- readonly image: import("./media.js").Asset;
281
+ readonly image: Media.Asset;
269
282
  } | {
270
283
  readonly type: "image";
271
284
  readonly index: number;
272
- readonly image: import("./media.js").Asset;
285
+ readonly image: Media.Asset;
273
286
  } | {
274
287
  readonly type: "finish";
275
288
  readonly providerMetadata?: {
@@ -327,7 +340,7 @@ export declare const make: (options?: Options) => {
327
340
  } | {
328
341
  readonly type: "video";
329
342
  readonly index: number;
330
- readonly video: import("./media.js").Asset;
343
+ readonly video: Media.Asset;
331
344
  } | {
332
345
  readonly type: "finish";
333
346
  readonly providerMetadata?: {
@@ -382,7 +395,7 @@ export declare const make: (options?: Options) => {
382
395
  }[];
383
396
  } | {
384
397
  readonly type: "finish";
385
- readonly audio: import("./media.js").Asset;
398
+ readonly audio: Media.Asset;
386
399
  readonly providerMetadata?: {
387
400
  readonly [x: string]: {
388
401
  readonly [x: string]: unknown;
@@ -505,6 +518,15 @@ export type Client = ReturnType<typeof make>;
505
518
  /** Default client over `RequestExecutor.fetchLayer` for scripts; the runtime builds its layer on first use. */
506
519
  export declare const ai: {
507
520
  run: <A, E>(effect: Effect.Effect<A, E, Services>, options?: RunOptions) => Promise<A>;
521
+ /** Decoded asset bytes, downloading `url` sources through the executor. */
522
+ bytes: (asset: Media.Asset, options?: RunOptions) => Promise<Uint8Array<ArrayBufferLike>>;
523
+ base64: (asset: Media.Asset, options?: RunOptions) => Promise<string>;
524
+ /** Pull a `url` asset into owned bytes before the provider URL expires. */
525
+ materialize: (asset: Media.Asset, options?: RunOptions) => Promise<Media.Asset>;
526
+ /** Read a file into an asset like `Media.file`: sniffed media type, then the extension's. */
527
+ file: (path: string, options?: Media.AssetOptions & RunOptions) => Promise<Media.Asset>;
528
+ /** Write an asset's bytes like `Media.write`, downloading `url` sources through the executor. */
529
+ write: (asset: Media.Asset, path: string, options?: RunOptions) => Promise<void>;
508
530
  llm: {
509
531
  request: <const SelectedLanguageModel extends LanguageModel>(input: RequestInput<SelectedLanguageModel>) => LLMRequest<SelectedLanguageModel>;
510
532
  generate: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) => Promise<import("./index.js").LLMResponse>;
@@ -679,7 +701,7 @@ export declare const ai: {
679
701
  readonly namespace?: string | undefined;
680
702
  } | {
681
703
  readonly type: "media";
682
- readonly media: import("./media.js").Asset;
704
+ readonly media: Media.Asset;
683
705
  readonly providerMetadata?: {
684
706
  readonly [x: string]: {
685
707
  readonly [x: string]: unknown;
@@ -735,11 +757,11 @@ export declare const ai: {
735
757
  } | {
736
758
  readonly type: "image-partial";
737
759
  readonly index: number;
738
- readonly image: import("./media.js").Asset;
760
+ readonly image: Media.Asset;
739
761
  } | {
740
762
  readonly type: "image";
741
763
  readonly index: number;
742
- readonly image: import("./media.js").Asset;
764
+ readonly image: Media.Asset;
743
765
  } | {
744
766
  readonly type: "finish";
745
767
  readonly providerMetadata?: {
@@ -797,7 +819,7 @@ export declare const ai: {
797
819
  } | {
798
820
  readonly type: "video";
799
821
  readonly index: number;
800
- readonly video: import("./media.js").Asset;
822
+ readonly video: Media.Asset;
801
823
  } | {
802
824
  readonly type: "finish";
803
825
  readonly providerMetadata?: {
@@ -852,7 +874,7 @@ export declare const ai: {
852
874
  }[];
853
875
  } | {
854
876
  readonly type: "finish";
855
- readonly audio: import("./media.js").Asset;
877
+ readonly audio: Media.Asset;
856
878
  readonly providerMetadata?: {
857
879
  readonly [x: string]: {
858
880
  readonly [x: string]: unknown;
package/dist/promise.js CHANGED
@@ -1,16 +1,15 @@
1
1
  import { Effect, Layer, ManagedRuntime, Stream } from "effect";
2
+ import { AIClient } from "./ai-client.js";
2
3
  import { Image, ImageModel, ImageRequest } from "./image.js";
3
- import { ImageClient } from "./image-client.js";
4
4
  import { LLM } from "./index.js";
5
- import { LLMClient } from "./route/client.js";
5
+ import { Media } from "./media.js";
6
+ import { tryRequest } from "./media-model.js";
6
7
  import { RequestExecutor } from "./route/executor.js";
7
- import { LanguageModel, LLMRequest } from "./schema/index.js";
8
+ import { AIError, InvalidRequestError, LanguageModel, LLMRequest } from "./schema/index.js";
8
9
  import { Speech, SpeechModel, SpeechRequest } from "./speech.js";
9
- import { SpeechClient } from "./speech-client.js";
10
10
  import { Transcription, TranscriptionModel, TranscriptionRequest, } from "./transcription.js";
11
- import { TranscriptionClient } from "./transcription-client.js";
11
+ import { fileMediaType } from "./utils/media-type.js";
12
12
  import { Video, VideoModel, VideoRequest } from "./video.js";
13
- import { VideoClient } from "./video-client.js";
14
13
  const abortEffect = (signal) => signal === undefined
15
14
  ? Effect.never
16
15
  : Effect.callback((resume) => {
@@ -23,29 +22,55 @@ const abortEffect = (signal) => signal === undefined
23
22
  return Effect.sync(() => signal.removeEventListener("abort", onAbort));
24
23
  });
25
24
  export const make = (options = {}) => {
26
- const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer, SpeechClient.layer, TranscriptionClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
27
- /** Run any package Effect (for example `asset.bytes()`) inside this runtime. */
25
+ const runtime = ManagedRuntime.make(AIClient.layerWith(options.layer ?? RequestExecutor.fetchLayer));
26
+ /** Run any package Effect (for example `LLMClient.compact(...)`) inside this runtime. */
28
27
  const run = (effect, options) => runtime.runPromise(effect, { signal: options?.signal });
29
28
  const iterate = (stream, options) => Stream.toAsyncIterable(Stream.unwrap(runtime.contextEffect.pipe(Effect.map((context) => stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context))))));
30
29
  const handle = (generation) => ({
31
30
  ...generation.snapshot,
32
31
  token: generation.token,
33
32
  await: (options) => run(generation.await({ poll: options?.poll }), options),
33
+ events: (options) => iterate(generation.events({ poll: options?.poll }), options),
34
+ result: (options) => run(generation.result(), options),
34
35
  refresh: (options) => run(generation.refresh(), options).then(handle),
35
36
  cancel: (options) => run(generation.cancel(), options),
36
37
  });
37
38
  // The typed `generate`/`stream` overloads take a concrete input or a request, not the union; normalize once here.
38
- const llmRequest = (input) => (input instanceof LLMRequest ? input : LLM.request(input));
39
+ const llmRequest = (input) => input instanceof LLMRequest ? Effect.succeed(input) : tryRequest(() => LLM.request(input));
39
40
  const imageRequest = (input) => input instanceof ImageRequest ? input : Image.request(input);
40
41
  const videoRequest = (input) => input instanceof VideoRequest ? input : Video.request(input);
41
42
  const speechRequest = (input) => input instanceof SpeechRequest ? input : Speech.request(input);
42
43
  const transcriptionRequest = (input) => input instanceof TranscriptionRequest ? input : Transcription.request(input);
43
44
  return {
44
45
  run,
46
+ /** Decoded asset bytes, downloading `url` sources through the executor. */
47
+ bytes: (asset, options) => run(asset.bytes(), options),
48
+ base64: (asset, options) => run(asset.base64(), options),
49
+ /** Pull a `url` asset into owned bytes before the provider URL expires. */
50
+ materialize: (asset, options) => run(asset.materialize(), options),
51
+ /** Read a file into an asset like `Media.file`: sniffed media type, then the extension's. */
52
+ file: async (path, options) => {
53
+ const { readFile } = await import("node:fs/promises");
54
+ return run(Effect.tryPromise({
55
+ try: (signal) => readFile(path, { signal }),
56
+ catch: (cause) => fileError(`Failed to read media file ${path}`, cause),
57
+ }).pipe(Effect.map((buffer) => {
58
+ const data = new Uint8Array(buffer);
59
+ return Media.bytes(data, fileMediaType(data, path), options);
60
+ })), options);
61
+ },
62
+ /** Write an asset's bytes like `Media.write`, downloading `url` sources through the executor. */
63
+ write: async (asset, path, options) => {
64
+ const { writeFile } = await import("node:fs/promises");
65
+ return run(asset.bytes().pipe(Effect.flatMap((data) => Effect.tryPromise({
66
+ try: (signal) => writeFile(path, data, { signal }),
67
+ catch: (cause) => fileError(`Failed to write media file ${path}`, cause),
68
+ }))), options);
69
+ },
45
70
  llm: {
46
71
  request: LLM.request,
47
- generate: (input, options) => run(LLM.generate(llmRequest(input)), options),
48
- stream: (input, options) => iterate(LLM.stream(llmRequest(input)), options),
72
+ generate: (input, options) => run(Effect.flatMap(llmRequest(input), LLM.generate), options),
73
+ stream: (input, options) => iterate(Stream.unwrap(Effect.map(llmRequest(input), LLM.stream)), options),
49
74
  },
50
75
  image: {
51
76
  request: Image.request,
@@ -76,6 +101,7 @@ export const make = (options = {}) => {
76
101
  dispose: () => runtime.dispose(),
77
102
  };
78
103
  };
104
+ const fileError = (message, cause) => new AIError({ reason: new InvalidRequestError({ message, cause }) });
79
105
  /** Default client over `RequestExecutor.fetchLayer` for scripts; the runtime builds its layer on first use. */
80
106
  export const ai = make();
81
107
  export * as AI from "./promise.js";
@@ -5,7 +5,11 @@ import { SpeechModel, type SpeechRequestFor } from "../speech.js";
5
5
  import { SpeechStream } from "./utils/speech-stream.js";
6
6
  export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
7
7
  export declare const PATH = "/audio/speech";
8
- export type OpenAISpeechOptions = Record<string, unknown>;
8
+ /** `voice`, `instructions`, `speed`, and `format` are common request fields; other native body fields pass through. */
9
+ export type OpenAISpeechOptions = {
10
+ /** Defaults to `"sse"` in `stream` mode on models that support it; the merged value selects the response framing. */
11
+ readonly stream_format?: "sse" | "audio";
12
+ } & Record<string, unknown>;
9
13
  export type Request = SpeechRequestFor<OpenAISpeechOptions>;
10
14
  interface State extends SpeechStream.Audio {
11
15
  readonly done: boolean;
@@ -149,7 +149,8 @@ export const mediaReference = (asset, provider, label) => {
149
149
  if (provider !== undefined && asset.source.type === "ref" && asset.source.provider === provider)
150
150
  return Effect.succeed({ type: "ref", value: asset.source.id });
151
151
  const accepted = provider === undefined ? "" : `, and ${provider} references`;
152
- return Effect.fail(invalidRequest(`${label} accepts inline bytes, data URLs, http(s) URLs${accepted}`));
152
+ const got = asset.source.type === "ref" ? `; got ${asset.source.provider}:${asset.source.id}` : "";
153
+ return Effect.fail(invalidRequest(`${label} accepts inline bytes, data URLs, http(s) URLs${accepted}${got}`));
153
154
  };
154
155
  /**
155
156
  * Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
@@ -4,7 +4,7 @@ import { AIError, type HttpOptions } from "../schema/index.js";
4
4
  export declare class MissingCredentialError extends Error {
5
5
  readonly source: string;
6
6
  readonly _tag = "MissingCredentialError";
7
- constructor(source: string);
7
+ constructor(source: string, message?: string);
8
8
  }
9
9
  export type CredentialError = MissingCredentialError | Config.ConfigError;
10
10
  export type AuthError = CredentialError | AIError;
@@ -1,11 +1,11 @@
1
- import { Config, Effect, Redacted } from "effect";
1
+ import { Config, Effect, Option, Redacted } from "effect";
2
2
  import { Headers } from "effect/unstable/http";
3
- import { AuthenticationError, InvalidRequestError, AIError } from "../schema/index.js";
3
+ import { AuthenticationError, AIError } from "../schema/index.js";
4
4
  export class MissingCredentialError extends Error {
5
5
  source;
6
6
  _tag = "MissingCredentialError";
7
- constructor(source) {
8
- super(`Missing auth credential: ${source}`);
7
+ constructor(source, message = `Missing auth credential: ${source}`) {
8
+ super(message);
9
9
  this.source = source;
10
10
  }
11
11
  }
@@ -47,7 +47,12 @@ export const value = (secret, source = "value") => credentialFromSecret(secret,
47
47
  export const optional = (secret, source = "optional value") => secret === undefined
48
48
  ? credential(Effect.fail(new MissingCredentialError(source)))
49
49
  : credentialFromSecret(secret, source);
50
- export const config = (name) => credentialFromSecret(Config.redacted(name), name);
50
+ export const config = (name) => credential(Effect.gen(function* () {
51
+ const secret = yield* Config.option(Config.redacted(name));
52
+ if (Option.isSome(secret) && Redacted.value(secret.value) !== "")
53
+ return secret.value;
54
+ return yield* Effect.fail(new MissingCredentialError(name, `${name} is not set`));
55
+ }));
51
56
  export const effect = (load) => credential(load);
52
57
  export const none = auth((input) => Effect.succeed(input.headers));
53
58
  export const headers = (input) => auth((inputAuth) => Effect.succeed(Headers.setAll(inputAuth.headers, input)));
@@ -80,14 +85,10 @@ export function scheme(name, source) {
80
85
  return render(source);
81
86
  }
82
87
  const toAIError = (error) => {
83
- if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
84
- return new AIError({
85
- reason: error instanceof MissingCredentialError
86
- ? new AuthenticationError({ message: error.message, cause: error })
87
- : new InvalidRequestError({ message: `Failed to resolve auth config: ${error.message}`, cause: error }),
88
- });
89
- }
90
- return error;
88
+ if (error instanceof AIError)
89
+ return error;
90
+ const message = error instanceof MissingCredentialError ? error.message : `Failed to resolve auth config: ${error.message}`;
91
+ return new AIError({ reason: new AuthenticationError({ message, cause: error }) });
91
92
  };
92
93
  export const toEffect = (input) => (authInput) => input.apply(authInput).pipe(Effect.mapError(toAIError));
93
94
  export * as Auth from "./auth.js";
@@ -121,9 +121,11 @@ export declare function canCompact(request: LLMRequest, options?: {
121
121
  export declare function canCompact(request: LLMRequest, options: {
122
122
  readonly mechanism: "trigger";
123
123
  }): request is CheckpointRequest;
124
- declare const Service_base: Context.ServiceClass<Service, "@opencode/LLMClient", Interface>;
125
- export declare class Service extends Service_base {
124
+ declare const LLMClientService_base: Context.ServiceClass<LLMClientService, "@opencode/LLMClient", Interface>;
125
+ export declare class LLMClientService extends LLMClientService_base {
126
126
  }
127
+ export declare const Service: typeof LLMClientService;
128
+ export type Service = LLMClientService;
127
129
  export interface MakeInput<Body, Frame, Event, State> {
128
130
  readonly compact?: CompactionOperations;
129
131
  /** Route id used in diagnostics and prepared request metadata. */
@@ -423,7 +425,7 @@ export declare const streamRequest: (request: LLMRequest, options?: StreamOption
423
425
  readonly [x: string]: unknown;
424
426
  };
425
427
  } | undefined;
426
- }, AIError, Service>;
428
+ }, AIError, LLMClientService>;
427
429
  export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
428
430
  export declare const Route: {
429
431
  readonly make: typeof make;
@@ -431,8 +433,8 @@ export declare const Route: {
431
433
  export declare const LLMClient: {
432
434
  readonly canCompact: typeof canCompact;
433
435
  readonly compact: typeof compact;
434
- readonly Service: typeof Service;
435
- readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
436
+ readonly Service: typeof LLMClientService;
437
+ readonly layer: Layer.Layer<LLMClientService, never, import("./executor-service.js").RequestExecutorService>;
436
438
  readonly stream: typeof stream;
437
439
  readonly generate: typeof generate;
438
440
  };
@@ -66,8 +66,9 @@ const unsupportedCompaction = (request, mechanism) => {
66
66
  message: `${request.model.provider}/${request.model.route.id} does not support ${mechanism === "trigger" ? "trigger" : "explicit"} compaction`,
67
67
  });
68
68
  };
69
- export class Service extends Context.Service()("@opencode/LLMClient") {
69
+ export class LLMClientService extends Context.Service()("@opencode/LLMClient") {
70
70
  }
71
+ export const Service = LLMClientService;
71
72
  const resolveRequestOptions = (request) => {
72
73
  const messages = normalizeToolHistory(request.messages);
73
74
  const normalized = messages === request.messages ? request : LLMRequest.update(request, { messages });
@@ -6,7 +6,9 @@ export interface Interface {
6
6
  }
7
7
  export type HttpHandler = (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
8
8
  export type HttpMiddleware = (request: HttpClientRequest.HttpClientRequest, handler: HttpHandler) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error>;
9
- declare const Service_base: Context.ServiceClass<Service, "@opencode/AI/RequestExecutor", Interface>;
10
- export declare class Service extends Service_base {
9
+ declare const RequestExecutorService_base: Context.ServiceClass<RequestExecutorService, "@opencode/AI/RequestExecutor", Interface>;
10
+ export declare class RequestExecutorService extends RequestExecutorService_base {
11
11
  }
12
+ export declare const Service: typeof RequestExecutorService;
13
+ export type Service = RequestExecutorService;
12
14
  export {};
@@ -1,3 +1,4 @@
1
1
  import { Context } from "effect";
2
- export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {
2
+ export class RequestExecutorService extends Context.Service()("@opencode/AI/RequestExecutor") {
3
3
  }
4
+ export const Service = RequestExecutorService;
@@ -18,5 +18,7 @@ export declare const httpFailure: (input: {
18
18
  export declare const responseStream: (response: HttpClientResponse.HttpClientResponse) => Stream.Stream<Uint8Array, AIError>;
19
19
  export declare const stream: (executor: Interface, request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Stream.Stream<Uint8Array, AIError>;
20
20
  export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>;
21
- export declare const fetchLayer: Layer.Layer<Service, never, never>;
21
+ export declare const fetchLayer: Layer.Layer<import("./executor-service.js").RequestExecutorService, never, never>;
22
+ /** Run `fn` on every request: it sees the raw response before status classification, inside middleware already on `executor`, and outside per-call middleware. */
23
+ export declare const middleware: (fn: HttpMiddleware, executor?: Layer.Layer<Service>) => Layer.Layer<Service>;
22
24
  export * as RequestExecutor from "./executor.js";
@@ -180,4 +180,11 @@ export const layer = Layer.effect(Service, Effect.gen(function* () {
180
180
  });
181
181
  }));
182
182
  export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer));
183
+ /** Run `fn` on every request: it sees the raw response before status classification, inside middleware already on `executor`, and outside per-call middleware. */
184
+ export const middleware = (fn, executor = fetchLayer) => Layer.effect(Service, Effect.gen(function* () {
185
+ const inner = yield* Service;
186
+ return Service.of({
187
+ execute: (request, next) => inner.execute(request, next === undefined ? fn : (input, handler) => fn(input, (forwarded) => next(forwarded, handler))),
188
+ });
189
+ })).pipe(Layer.provide(executor));
183
190
  export * as RequestExecutor from "./executor.js";
@@ -1,5 +1,5 @@
1
1
  export { Route, LLMClient } from "./client.js";
2
- export type { Route as RouteShape, RouteLanguageModelInput, RouteRoutedLanguageModelInput, RouteDefaults, RouteDefaultsInput, AnyRoute, Interface as LLMClientShape, Service as LLMClientService, StreamOptions, CompactMethod, CompactionOperations, CompactionRequest, CheckpointRequest, EndpointCompactOptions, TriggerCompactOptions, } from "./client.js";
2
+ export type { Route as RouteShape, RouteLanguageModelInput, RouteRoutedLanguageModelInput, RouteDefaults, RouteDefaultsInput, AnyRoute, Interface as LLMClientShape, LLMClientService, StreamOptions, CompactMethod, CompactionOperations, CompactionRequest, CheckpointRequest, EndpointCompactOptions, TriggerCompactOptions, } from "./client.js";
3
3
  export * from "./executor.js";
4
4
  export { Auth } from "./auth.js";
5
5
  export { AuthOptions } from "./auth-options.js";
@@ -6,15 +6,17 @@ export interface Interface {
6
6
  readonly generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError>;
7
7
  readonly stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError>;
8
8
  }
9
- declare const Service_base: Context.ServiceClass<Service, "@opencode/SpeechClient", Interface>;
10
- export declare class Service extends Service_base {
9
+ declare const SpeechClientService_base: Context.ServiceClass<SpeechClientService, "@opencode/SpeechClient", Interface>;
10
+ export declare class SpeechClientService extends SpeechClientService_base {
11
11
  }
12
+ export declare const Service: typeof SpeechClientService;
13
+ export type Service = SpeechClientService;
12
14
  export declare const generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError, Service>;
13
15
  export declare const stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError, Service>;
14
16
  export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
15
17
  export declare const SpeechClient: {
16
- readonly Service: typeof Service;
17
- readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
18
+ readonly Service: typeof SpeechClientService;
19
+ readonly layer: Layer.Layer<SpeechClientService, never, import("./route/executor-service.js").RequestExecutorService>;
18
20
  readonly generate: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Effect.Effect<SpeechResponse, AIError, Service>;
19
21
  readonly stream: <Options extends SpeechOptions>(request: SpeechRequestFor<Options>) => Stream.Stream<SpeechEvent, AIError, Service>;
20
22
  };
@@ -1,7 +1,8 @@
1
1
  import { Context, Effect, Layer, Stream } from "effect";
2
2
  import { RequestExecutor } from "./route/executor.js";
3
- export class Service extends Context.Service()("@opencode/SpeechClient") {
3
+ export class SpeechClientService extends Context.Service()("@opencode/SpeechClient") {
4
4
  }
5
+ export const Service = SpeechClientService;
5
6
  export const generate = (request) => Effect.gen(function* () {
6
7
  const client = yield* Service;
7
8
  return yield* client.generate(request);
package/dist/testing.d.ts CHANGED
@@ -1743,11 +1743,11 @@ export declare const hangAfter: (...events: readonly LLMEvent[]) => Stream.Strea
1743
1743
  } | undefined;
1744
1744
  }, never, never>;
1745
1745
  /** Provides one shared implementation under the normal client and test-control tags. */
1746
- export declare const testLayer: (options?: LayerOptions) => Layer.Layer<import("./route/client.js").Service | Test, never, never>;
1746
+ export declare const testLayer: (options?: LayerOptions) => Layer.Layer<import("./route/client.js").LLMClientService | Test, never, never>;
1747
1747
  /** @deprecated Use testLayer; retained for published callers of the legacy control interface. */
1748
1748
  export declare const layer: (options?: LayerOptions) => Layer.Layer<Service, never, never>;
1749
1749
  /** @deprecated testLayer provides LLMClient.Service directly. */
1750
- export declare const clientLayer: Layer.Layer<import("./route/client.js").Service, never, Service>;
1750
+ export declare const clientLayer: Layer.Layer<import("./route/client.js").LLMClientService, never, Service>;
1751
1751
  export declare const push: (...responses: readonly Response[]) => Effect.Effect<void, never, Service>;
1752
1752
  export declare const always: (response: Response) => Effect.Effect<void, never, Service>;
1753
1753
  export declare const wait: (count: number) => Effect.Effect<void, never, Service>;
@@ -9,17 +9,19 @@ export interface Interface {
9
9
  readonly start: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>) => Effect.Effect<Generation<TranscriptionResponse>, AIError>;
10
10
  readonly resume: <Options extends TranscriptionOptions>(model: TranscriptionModel<Options>, token: unknown) => Effect.Effect<Generation<TranscriptionResponse>, AIError>;
11
11
  }
12
- declare const Service_base: Context.ServiceClass<Service, "@opencode/TranscriptionClient", Interface>;
13
- export declare class Service extends Service_base {
12
+ declare const TranscriptionClientService_base: Context.ServiceClass<TranscriptionClientService, "@opencode/TranscriptionClient", Interface>;
13
+ export declare class TranscriptionClientService extends TranscriptionClientService_base {
14
14
  }
15
+ export declare const Service: typeof TranscriptionClientService;
16
+ export type Service = TranscriptionClientService;
15
17
  export declare const generate: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>, options?: AwaitOptions) => Effect.Effect<TranscriptionResponse, AIError, Service>;
16
18
  export declare const stream: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>, options?: AwaitOptions) => Stream.Stream<TranscriptionEvent, AIError, Service>;
17
19
  export declare const start: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>) => Effect.Effect<Generation<TranscriptionResponse>, AIError, Service>;
18
20
  export declare const resume: <Options extends TranscriptionOptions>(model: TranscriptionModel<Options>, token: unknown) => Effect.Effect<Generation<TranscriptionResponse>, AIError, Service>;
19
21
  export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
20
22
  export declare const TranscriptionClient: {
21
- readonly Service: typeof Service;
22
- readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
23
+ readonly Service: typeof TranscriptionClientService;
24
+ readonly layer: Layer.Layer<TranscriptionClientService, never, import("./route/executor-service.js").RequestExecutorService>;
23
25
  readonly generate: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>, options?: AwaitOptions) => Effect.Effect<TranscriptionResponse, AIError, Service>;
24
26
  readonly stream: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>, options?: AwaitOptions) => Stream.Stream<TranscriptionEvent, AIError, Service>;
25
27
  readonly start: <Options extends TranscriptionOptions>(request: TranscriptionRequestFor<Options>) => Effect.Effect<Generation<TranscriptionResponse>, AIError, Service>;
@@ -2,8 +2,9 @@ import { Context, Effect, Layer, Stream } from "effect";
2
2
  import { RequestExecutor } from "./route/executor.js";
3
3
  import { MediaRoute } from "./route/media.js";
4
4
  import { responseEvents, } from "./transcription.js";
5
- export class Service extends Context.Service()("@opencode/TranscriptionClient") {
5
+ export class TranscriptionClientService extends Context.Service()("@opencode/TranscriptionClient") {
6
6
  }
7
+ export const Service = TranscriptionClientService;
7
8
  export const generate = (request, options) => Effect.gen(function* () {
8
9
  const client = yield* Service;
9
10
  return yield* client.generate(request, options);
@@ -1012,7 +1012,7 @@ export declare const TranscriptionEvent: Schema.Union<readonly [Schema.Struct<{
1012
1012
  readonly confidence?: number | undefined;
1013
1013
  }[] | undefined;
1014
1014
  }) => any) | undefined;
1015
- } & { [K in Exclude<keyof Cases, "finish" | "text-delta" | "generation-queued" | "generation-progress" | "segment">]: never; }, OrElse extends (value: Exclude<{
1015
+ } & { [K in Exclude<keyof Cases, "finish" | "generation-queued" | "generation-progress" | "text-delta" | "segment">]: never; }, OrElse extends (value: Exclude<{
1016
1016
  readonly id: string;
1017
1017
  readonly type: "generation-queued";
1018
1018
  readonly position?: number | undefined;
@@ -1245,7 +1245,7 @@ export declare const TranscriptionEvent: Schema.Union<readonly [Schema.Struct<{
1245
1245
  readonly confidence?: number | undefined;
1246
1246
  }[] | undefined;
1247
1247
  }) => any) | undefined;
1248
- } & { [K in Exclude<keyof Cases, "finish" | "text-delta" | "generation-queued" | "generation-progress" | "segment">]: never; }, OrElse extends (value: Exclude<{
1248
+ } & { [K in Exclude<keyof Cases, "finish" | "generation-queued" | "generation-progress" | "text-delta" | "segment">]: never; }, OrElse extends (value: Exclude<{
1249
1249
  readonly id: string;
1250
1250
  readonly type: "generation-queued";
1251
1251
  readonly position?: number | undefined;
@@ -3,5 +3,6 @@
3
3
  * `undefined` so callers can fall back to a provider-declared type or `application/octet-stream`.
4
4
  */
5
5
  export declare const detectMediaType: (bytes: Uint8Array) => string | undefined;
6
- export declare const extensionMediaType: (path: string) => string | undefined;
6
+ /** Media type of a file's contents: sniffed magic bytes, then the path's extension. */
7
+ export declare const fileMediaType: (bytes: Uint8Array, path: string) => string | undefined;
7
8
  export declare const mediaTypeExtension: (mediaType: string) => string | undefined;
@@ -54,7 +54,9 @@ const EXTENSIONS = {
54
54
  md: "text/markdown",
55
55
  csv: "text/csv",
56
56
  };
57
- export const extensionMediaType = (path) => EXTENSIONS[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
57
+ const extensionMediaType = (path) => EXTENSIONS[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
58
+ /** Media type of a file's contents: sniffed magic bytes, then the path's extension. */
59
+ export const fileMediaType = (bytes, path) => detectMediaType(bytes) ?? extensionMediaType(path);
58
60
  const EXTENSION_ALIASES = {
59
61
  "audio/mp3": "mp3",
60
62
  "audio/m4a": "m4a",