@effect-uai/responses 0.6.0 → 0.7.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"OpenAIEmbedding.d.mts","names":[],"sources":["../src/OpenAIEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;;KA+BY,kBAAA,GAAqB,IAAA,CAAK,kBAAA;+EAE3B,KAAA,EAAO,oBAAA;AAAA;AAAA,KAGN,sBAAA,GAAyB,IAAA,CAAK,kBAAA;EAAA,SAC/B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,sBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC7E,SAAA,GACP,OAAA,EAAS,sBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,oBAAA;;;;;;cAOY,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,YAAA;AAAA;;;;;cA8LP,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;;;cAc9C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
1
+ {"version":3,"file":"OpenAIEmbedding.d.mts","names":[],"sources":["../src/OpenAIEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;;KAiCY,kBAAA,GAAqB,IAAA,CAAK,kBAAA;+EAE3B,KAAA,EAAO,oBAAA;AAAA;AAAA,KAGN,sBAAA,GAAyB,IAAA,CAAK,kBAAA;EAAA,SAC/B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,sBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC7E,SAAA,GACP,OAAA,EAAS,sBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,oBAAA;;;;;;cAOY,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,YAAA;AAAA;;;;;cAsMP,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;;;cAc9C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
@@ -3,7 +3,8 @@ import { resolveHost } from "./region.mjs";
3
3
  import { Context, Effect, Layer, Schema } from "effect";
4
4
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
5
5
  import * as AiError from "@effect-uai/core/AiError";
6
- import { EmbeddingModel } from "@effect-uai/core/EmbeddingModel";
6
+ import * as Capabilities from "@effect-uai/core/Capabilities";
7
+ import { EmbeddingModel, assertEncoding } from "@effect-uai/core/EmbeddingModel";
7
8
  //#region src/OpenAIEmbedding.ts
8
9
  var OpenAIEmbedding_exports = /* @__PURE__ */ __exportAll({
9
10
  OpenAIEmbedding: () => OpenAIEmbedding,
@@ -16,22 +17,22 @@ var OpenAIEmbedding_exports = /* @__PURE__ */ __exportAll({
16
17
  * code. Both are registered by `layer`.
17
18
  */
18
19
  var OpenAIEmbedding = class extends Context.Service()("@betalyra/effect-uai/providers/responses/OpenAIEmbedding") {};
19
- const imageRejected = (param) => new AiError.InvalidRequest({
20
+ const imageRejected = new AiError.Unsupported({
20
21
  provider: "openai",
21
- param,
22
- raw: "OpenAI embeddings are text-only; pass a string or { text }"
22
+ capability: "imageEmbedding",
23
+ reason: "OpenAI embeddings are text-only; pass a string or { text }."
23
24
  });
24
- const partToText = (param, part) => "text" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected(param));
25
+ const partToText = (part) => "text" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected);
25
26
  /**
26
27
  * OpenAI accepts a single string per input. Pure-text `content[]` is
27
28
  * concatenated with newlines (treats them as paragraphs of one document);
28
- * any image part fails with `InvalidRequest`.
29
+ * any image part fails with `Unsupported`.
29
30
  */
30
31
  const inputToString = (input) => {
31
32
  if (typeof input === "string") return Effect.succeed(input);
32
33
  if ("text" in input) return Effect.succeed(input.text);
33
- if ("image" in input) return Effect.fail(imageRejected("input.image"));
34
- return Effect.forEach(input.content, (p, i) => partToText(`input.content[${i}].image`, p)).pipe(Effect.map((texts) => texts.join("\n")));
34
+ if ("image" in input) return Effect.fail(imageRejected);
35
+ return Effect.forEach(input.content, partToText).pipe(Effect.map((texts) => texts.join("\n")));
35
36
  };
36
37
  const buildSingleBody = (request) => inputToString(request.input).pipe(Effect.map((input) => ({
37
38
  model: request.model,
@@ -117,6 +118,12 @@ const postEmbed = (cfg, body) => Effect.gen(function* () {
117
118
  return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
118
119
  });
119
120
  const usageOf = (u) => ({ inputTokens: u.prompt_tokens });
121
+ const warnTaskDropped = (task) => Capabilities.warnDroppedWhen(task, {
122
+ provider: "openai",
123
+ capability: "task",
124
+ field: "task",
125
+ reason: "OpenAI embeddings have no task-type parameter."
126
+ });
120
127
  const embedImpl = (cfg) => (request) => buildSingleBody(request).pipe(Effect.flatMap((body) => postEmbed(cfg, body)), Effect.flatMap((decoded) => {
121
128
  const first = decoded.data[0];
122
129
  if (first === void 0) return Effect.fail(transportFailure("OpenAI returned empty `data` array"));
@@ -146,8 +153,8 @@ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => (
146
153
  const layer = (cfg) => {
147
154
  const typed = Layer.effect(OpenAIEmbedding, make(cfg));
148
155
  const generic = Layer.effect(EmbeddingModel, Effect.map(make(cfg), (s) => ({
149
- embed: (req) => s.embed(req),
150
- embedMany: (req) => s.embedMany(req)
156
+ embed: (req) => assertEncoding(req.encoding, ["float32"], "openai").pipe(Effect.andThen(warnTaskDropped(req.task)), Effect.andThen(s.embed(req))),
157
+ embedMany: (req) => assertEncoding(req.encoding, ["float32"], "openai").pipe(Effect.andThen(warnTaskDropped(req.task)), Effect.andThen(s.embedMany(req)))
151
158
  })));
152
159
  return Layer.merge(typed, generic);
153
160
  };
@@ -1 +1 @@
1
- {"version":3,"file":"OpenAIEmbedding.mjs","names":[],"sources":["../src/OpenAIEmbedding.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type {\n EmbedContentPart,\n EmbedInput,\n Float32Embedding,\n Usage,\n} from \"@effect-uai/core/Embedding\"\nimport {\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n type EmbedEncoding,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { OpenAIEmbeddingModel } from \"./models.js\"\nimport { type OpenAiRegion, resolveHost } from \"./region.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * OpenAI's embedding API has no task-type semantics, so `task` is omitted\n * from the request type intentionally - passing it would be a compile\n * error. The generic `EmbeddingModel` registration accepts and silently\n * ignores `task` from `CommonEmbedRequest`.\n */\nexport type OpenAIEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed OpenAI union. */\n readonly model: OpenAIEmbeddingModel\n}\n\nexport type OpenAIEmbedManyRequest = Omit<OpenAIEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type OpenAIEmbeddingService = {\n readonly embed: (request: OpenAIEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: OpenAIEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for OpenAI-typed embedding\n * requests; yield the generic `EmbeddingModel` tag for provider-portable\n * code. Both are registered by `layer`.\n */\nexport class OpenAIEmbedding extends Context.Service<OpenAIEmbedding, OpenAIEmbeddingService>()(\n \"@betalyra/effect-uai/providers/responses/OpenAIEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: OpenAiRegion\n}\n\n// ---------------------------------------------------------------------------\n// Codec - input flattening to the single string OpenAI expects\n// ---------------------------------------------------------------------------\n\nconst imageRejected = (param: string): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"openai\",\n param,\n raw: \"OpenAI embeddings are text-only; pass a string or { text }\",\n })\n\nconst partToText = (\n param: string,\n part: EmbedContentPart,\n): Effect.Effect<string, AiError.AiError> =>\n \"text\" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected(param))\n\n/**\n * OpenAI accepts a single string per input. Pure-text `content[]` is\n * concatenated with newlines (treats them as paragraphs of one document);\n * any image part fails with `InvalidRequest`.\n */\nconst inputToString = (input: EmbedInput): Effect.Effect<string, AiError.AiError> => {\n if (typeof input === \"string\") return Effect.succeed(input)\n if (\"text\" in input) return Effect.succeed(input.text)\n if (\"image\" in input) return Effect.fail(imageRejected(\"input.image\"))\n return Effect.forEach(input.content, (p, i) => partToText(`input.content[${i}].image`, p)).pipe(\n Effect.map((texts) => texts.join(\"\\n\")),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly model: string\n readonly input: string | ReadonlyArray<string>\n readonly dimensions?: number\n readonly encoding_format?: \"float\" | \"base64\"\n}\n\nconst buildSingleBody = (request: OpenAIEmbedRequest): Effect.Effect<WireBody, AiError.AiError> =>\n inputToString(request.input).pipe(\n Effect.map((input) => ({\n model: request.model,\n input,\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n })),\n )\n\nconst buildBatchBody = (\n request: OpenAIEmbedManyRequest,\n): Effect.Effect<WireBody, AiError.AiError> =>\n Effect.forEach(request.inputs, inputToString).pipe(\n Effect.map((inputs) => ({\n model: request.model,\n input: inputs,\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n })),\n )\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireEmbeddingItem = Schema.Struct({\n embedding: Schema.Array(Schema.Number),\n index: Schema.Number,\n})\n\nconst WireUsage = Schema.Struct({\n prompt_tokens: Schema.Number,\n total_tokens: Schema.Number,\n})\n\nconst WireResponse = Schema.Struct({\n data: Schema.Array(WireEmbeddingItem),\n model: Schema.String,\n usage: WireUsage,\n})\n\nconst valuesToEmbedding = (values: ReadonlyArray<number>): Float32Embedding => ({\n _tag: \"float32\",\n vector: Float32Array.from(values),\n})\n\n// OpenAI returns `data[]` unsorted in theory; sort by `index` so caller\n// gets results in the order they passed inputs.\nconst orderedEmbeddings = (\n data: ReadonlyArray<{ readonly embedding: ReadonlyArray<number>; readonly index: number }>,\n): ReadonlyArray<Float32Embedding> =>\n [...data].sort((a, b) => a.index - b.index).map((item) => valuesToEmbedding(item.embedding))\n\n// ---------------------------------------------------------------------------\n// HTTP errors\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"openai\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"openai\", raw: cause })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst postEmbed = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<typeof WireResponse.Type, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/embeddings`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst usageOf = (u: typeof WireUsage.Type): Usage => ({ inputTokens: u.prompt_tokens })\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: OpenAIEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildSingleBody(request).pipe(\n Effect.flatMap((body) => postEmbed(cfg, body)),\n Effect.flatMap((decoded) => {\n const first = decoded.data[0]\n if (first === undefined) {\n return Effect.fail(transportFailure(\"OpenAI returned empty `data` array\"))\n }\n return Effect.succeed<EmbedResponse>({\n embedding: valuesToEmbedding(first.embedding),\n usage: usageOf(decoded.usage),\n })\n }),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: OpenAIEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildBatchBody(request).pipe(\n Effect.flatMap((body) => postEmbed(cfg, body)),\n Effect.map(\n (decoded): EmbedManyResponse => ({\n embeddings: orderedEmbeddings(decoded.data),\n usage: usageOf(decoded.usage),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `OpenAIEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<OpenAIEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `OpenAIEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation. The generic registration trusts the caller; invalid\n * encodings produce an `InvalidRequest` from OpenAI's API.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<OpenAIEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(OpenAIEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n // OpenAI only emits float32; the cast is sound for the 99% case.\n // A caller asking for a non-float32 encoding via the generic tag\n // gets the type they requested but the runtime returns float32.\n embed: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedRequest, \"encoding\"> & { readonly encoding?: E },\n ) => s.embed(req as OpenAIEmbedRequest) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,\n embedMany: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedManyRequest, \"encoding\"> & { readonly encoding?: E },\n ) =>\n s.embedMany(req as OpenAIEmbedManyRequest) as Effect.Effect<\n EmbedManyResponse<E>,\n AiError.AiError\n >,\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"OpenAIEmbedding.mjs","names":[],"sources":["../src/OpenAIEmbedding.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport type {\n EmbedContentPart,\n EmbedInput,\n Float32Embedding,\n Usage,\n} from \"@effect-uai/core/Embedding\"\nimport {\n assertEncoding,\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n type EmbedEncoding,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { OpenAIEmbeddingModel } from \"./models.js\"\nimport { type OpenAiRegion, resolveHost } from \"./region.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * OpenAI's embedding API has no task-type semantics, so `task` is omitted\n * from the request type intentionally - passing it would be a compile\n * error. The generic `EmbeddingModel` registration accepts and silently\n * ignores `task` from `CommonEmbedRequest`.\n */\nexport type OpenAIEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed OpenAI union. */\n readonly model: OpenAIEmbeddingModel\n}\n\nexport type OpenAIEmbedManyRequest = Omit<OpenAIEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type OpenAIEmbeddingService = {\n readonly embed: (request: OpenAIEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: OpenAIEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for OpenAI-typed embedding\n * requests; yield the generic `EmbeddingModel` tag for provider-portable\n * code. Both are registered by `layer`.\n */\nexport class OpenAIEmbedding extends Context.Service<OpenAIEmbedding, OpenAIEmbeddingService>()(\n \"@betalyra/effect-uai/providers/responses/OpenAIEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: OpenAiRegion\n}\n\n// ---------------------------------------------------------------------------\n// Codec - input flattening to the single string OpenAI expects\n// ---------------------------------------------------------------------------\n\n// Image input on a text-only embedder is bucket 1: a silently dropped image\n// yields a vector that represents the wrong thing. Reject with `Unsupported`\n// (capability gap), not `InvalidRequest` (which is for wire-shape mismatches).\n// OpenAI's embeddings API remains text-only as of June 2026.\nconst imageRejected: AiError.AiError = new AiError.Unsupported({\n provider: \"openai\",\n capability: \"imageEmbedding\",\n reason: \"OpenAI embeddings are text-only; pass a string or { text }.\",\n})\n\nconst partToText = (part: EmbedContentPart): Effect.Effect<string, AiError.AiError> =>\n \"text\" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected)\n\n/**\n * OpenAI accepts a single string per input. Pure-text `content[]` is\n * concatenated with newlines (treats them as paragraphs of one document);\n * any image part fails with `Unsupported`.\n */\nconst inputToString = (input: EmbedInput): Effect.Effect<string, AiError.AiError> => {\n if (typeof input === \"string\") return Effect.succeed(input)\n if (\"text\" in input) return Effect.succeed(input.text)\n if (\"image\" in input) return Effect.fail(imageRejected)\n return Effect.forEach(input.content, partToText).pipe(Effect.map((texts) => texts.join(\"\\n\")))\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly model: string\n readonly input: string | ReadonlyArray<string>\n readonly dimensions?: number\n readonly encoding_format?: \"float\" | \"base64\"\n}\n\nconst buildSingleBody = (request: OpenAIEmbedRequest): Effect.Effect<WireBody, AiError.AiError> =>\n inputToString(request.input).pipe(\n Effect.map((input) => ({\n model: request.model,\n input,\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n })),\n )\n\nconst buildBatchBody = (\n request: OpenAIEmbedManyRequest,\n): Effect.Effect<WireBody, AiError.AiError> =>\n Effect.forEach(request.inputs, inputToString).pipe(\n Effect.map((inputs) => ({\n model: request.model,\n input: inputs,\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n })),\n )\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireEmbeddingItem = Schema.Struct({\n embedding: Schema.Array(Schema.Number),\n index: Schema.Number,\n})\n\nconst WireUsage = Schema.Struct({\n prompt_tokens: Schema.Number,\n total_tokens: Schema.Number,\n})\n\nconst WireResponse = Schema.Struct({\n data: Schema.Array(WireEmbeddingItem),\n model: Schema.String,\n usage: WireUsage,\n})\n\nconst valuesToEmbedding = (values: ReadonlyArray<number>): Float32Embedding => ({\n _tag: \"float32\",\n vector: Float32Array.from(values),\n})\n\n// OpenAI returns `data[]` unsorted in theory; sort by `index` so caller\n// gets results in the order they passed inputs.\nconst orderedEmbeddings = (\n data: ReadonlyArray<{ readonly embedding: ReadonlyArray<number>; readonly index: number }>,\n): ReadonlyArray<Float32Embedding> =>\n [...data].sort((a, b) => a.index - b.index).map((item) => valuesToEmbedding(item.embedding))\n\n// ---------------------------------------------------------------------------\n// HTTP errors\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"openai\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"openai\", raw: cause })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst postEmbed = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<typeof WireResponse.Type, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/embeddings`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst usageOf = (u: typeof WireUsage.Type): Usage => ({ inputTokens: u.prompt_tokens })\n\n// `task` is dropped on the generic path (OpenAI has no task parameter on any\n// model, so this is a per-Layer gap, not per-model). Bucket 2: warn, don't fail.\nconst warnTaskDropped = (task: CommonEmbedRequest[\"task\"]): Effect.Effect<void> =>\n Capabilities.warnDroppedWhen(task, {\n provider: \"openai\",\n capability: \"task\",\n field: \"task\",\n reason: \"OpenAI embeddings have no task-type parameter.\",\n })\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: OpenAIEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildSingleBody(request).pipe(\n Effect.flatMap((body) => postEmbed(cfg, body)),\n Effect.flatMap((decoded) => {\n const first = decoded.data[0]\n if (first === undefined) {\n return Effect.fail(transportFailure(\"OpenAI returned empty `data` array\"))\n }\n return Effect.succeed<EmbedResponse>({\n embedding: valuesToEmbedding(first.embedding),\n usage: usageOf(decoded.usage),\n })\n }),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: OpenAIEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildBatchBody(request).pipe(\n Effect.flatMap((body) => postEmbed(cfg, body)),\n Effect.map(\n (decoded): EmbedManyResponse => ({\n embeddings: orderedEmbeddings(decoded.data),\n usage: usageOf(decoded.usage),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build an `OpenAIEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<OpenAIEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `OpenAIEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation. The generic registration trusts the caller; invalid\n * encodings produce an `InvalidRequest` from OpenAI's API.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<OpenAIEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(OpenAIEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n // OpenAI emits float32 only and has no task parameter. Reject a\n // non-float32 `encoding` (bucket 1, else a float32 vector comes back\n // mislabeled as the requested type) and warn on a dropped `task`\n // (bucket 2).\n embed: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedRequest, \"encoding\"> & { readonly encoding?: E },\n ) =>\n assertEncoding(req.encoding, [\"float32\"], \"openai\").pipe(\n Effect.andThen(warnTaskDropped(req.task)),\n Effect.andThen(s.embed(req as OpenAIEmbedRequest)),\n ) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,\n embedMany: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedManyRequest, \"encoding\"> & { readonly encoding?: E },\n ) =>\n assertEncoding(req.encoding, [\"float32\"], \"openai\").pipe(\n Effect.andThen(warnTaskDropped(req.task)),\n Effect.andThen(s.embedMany(req as OpenAIEmbedManyRequest)),\n ) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/responses",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "OpenAI Responses API provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -49,7 +49,7 @@
49
49
  "devDependencies": {
50
50
  "effect": "4.0.0-beta.57",
51
51
  "typescript": "^6.0.3",
52
- "@effect-uai/core": "0.6.0"
52
+ "@effect-uai/core": "0.7.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@effect-uai/core": ">=0.2.0 <1",
@@ -1,6 +1,7 @@
1
1
  import { Context, Effect, Layer, Redacted, Schema } from "effect"
2
2
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
+ import * as Capabilities from "@effect-uai/core/Capabilities"
4
5
  import type {
5
6
  EmbedContentPart,
6
7
  EmbedInput,
@@ -8,6 +9,7 @@ import type {
8
9
  Usage,
9
10
  } from "@effect-uai/core/Embedding"
10
11
  import {
12
+ assertEncoding,
11
13
  type CommonEmbedManyRequest,
12
14
  type CommonEmbedRequest,
13
15
  type EmbedEncoding,
@@ -64,31 +66,29 @@ export type Config = {
64
66
  // Codec - input flattening to the single string OpenAI expects
65
67
  // ---------------------------------------------------------------------------
66
68
 
67
- const imageRejected = (param: string): AiError.AiError =>
68
- new AiError.InvalidRequest({
69
- provider: "openai",
70
- param,
71
- raw: "OpenAI embeddings are text-only; pass a string or { text }",
72
- })
69
+ // Image input on a text-only embedder is bucket 1: a silently dropped image
70
+ // yields a vector that represents the wrong thing. Reject with `Unsupported`
71
+ // (capability gap), not `InvalidRequest` (which is for wire-shape mismatches).
72
+ // OpenAI's embeddings API remains text-only as of June 2026.
73
+ const imageRejected: AiError.AiError = new AiError.Unsupported({
74
+ provider: "openai",
75
+ capability: "imageEmbedding",
76
+ reason: "OpenAI embeddings are text-only; pass a string or { text }.",
77
+ })
73
78
 
74
- const partToText = (
75
- param: string,
76
- part: EmbedContentPart,
77
- ): Effect.Effect<string, AiError.AiError> =>
78
- "text" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected(param))
79
+ const partToText = (part: EmbedContentPart): Effect.Effect<string, AiError.AiError> =>
80
+ "text" in part ? Effect.succeed(part.text) : Effect.fail(imageRejected)
79
81
 
80
82
  /**
81
83
  * OpenAI accepts a single string per input. Pure-text `content[]` is
82
84
  * concatenated with newlines (treats them as paragraphs of one document);
83
- * any image part fails with `InvalidRequest`.
85
+ * any image part fails with `Unsupported`.
84
86
  */
85
87
  const inputToString = (input: EmbedInput): Effect.Effect<string, AiError.AiError> => {
86
88
  if (typeof input === "string") return Effect.succeed(input)
87
89
  if ("text" in input) return Effect.succeed(input.text)
88
- if ("image" in input) return Effect.fail(imageRejected("input.image"))
89
- return Effect.forEach(input.content, (p, i) => partToText(`input.content[${i}].image`, p)).pipe(
90
- Effect.map((texts) => texts.join("\n")),
91
- )
90
+ if ("image" in input) return Effect.fail(imageRejected)
91
+ return Effect.forEach(input.content, partToText).pipe(Effect.map((texts) => texts.join("\n")))
92
92
  }
93
93
 
94
94
  // ---------------------------------------------------------------------------
@@ -205,6 +205,16 @@ const postEmbed = (
205
205
 
206
206
  const usageOf = (u: typeof WireUsage.Type): Usage => ({ inputTokens: u.prompt_tokens })
207
207
 
208
+ // `task` is dropped on the generic path (OpenAI has no task parameter on any
209
+ // model, so this is a per-Layer gap, not per-model). Bucket 2: warn, don't fail.
210
+ const warnTaskDropped = (task: CommonEmbedRequest["task"]): Effect.Effect<void> =>
211
+ Capabilities.warnDroppedWhen(task, {
212
+ provider: "openai",
213
+ capability: "task",
214
+ field: "task",
215
+ reason: "OpenAI embeddings have no task-type parameter.",
216
+ })
217
+
208
218
  const embedImpl =
209
219
  (cfg: Config) =>
210
220
  (
@@ -272,19 +282,24 @@ export const layer = (
272
282
  Effect.map(
273
283
  make(cfg),
274
284
  (s): EmbeddingModelService => ({
275
- // OpenAI only emits float32; the cast is sound for the 99% case.
276
- // A caller asking for a non-float32 encoding via the generic tag
277
- // gets the type they requested but the runtime returns float32.
285
+ // OpenAI emits float32 only and has no task parameter. Reject a
286
+ // non-float32 `encoding` (bucket 1, else a float32 vector comes back
287
+ // mislabeled as the requested type) and warn on a dropped `task`
288
+ // (bucket 2).
278
289
  embed: <E extends EmbedEncoding | undefined = undefined>(
279
290
  req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
280
- ) => s.embed(req as OpenAIEmbedRequest) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
291
+ ) =>
292
+ assertEncoding(req.encoding, ["float32"], "openai").pipe(
293
+ Effect.andThen(warnTaskDropped(req.task)),
294
+ Effect.andThen(s.embed(req as OpenAIEmbedRequest)),
295
+ ) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
281
296
  embedMany: <E extends EmbedEncoding | undefined = undefined>(
282
297
  req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
283
298
  ) =>
284
- s.embedMany(req as OpenAIEmbedManyRequest) as Effect.Effect<
285
- EmbedManyResponse<E>,
286
- AiError.AiError
287
- >,
299
+ assertEncoding(req.encoding, ["float32"], "openai").pipe(
300
+ Effect.andThen(warnTaskDropped(req.task)),
301
+ Effect.andThen(s.embedMany(req as OpenAIEmbedManyRequest)),
302
+ ) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
288
303
  }),
289
304
  ),
290
305
  )