@effect-uai/responses 0.5.2 → 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.
- package/dist/OpenAIEmbedding.d.mts +2 -0
- package/dist/OpenAIEmbedding.d.mts.map +1 -1
- package/dist/OpenAIEmbedding.mjs +19 -12
- package/dist/OpenAIEmbedding.mjs.map +1 -1
- package/dist/Responses.d.mts +2 -0
- package/dist/Responses.d.mts.map +1 -1
- package/dist/Responses.mjs +2 -1
- package/dist/Responses.mjs.map +1 -1
- package/dist/codec.d.mts +4 -4
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +1 -1
- package/dist/codec.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/magic-string.es-PAZgLX4h.mjs +1013 -0
- package/dist/magic-string.es-PAZgLX4h.mjs.map +1 -0
- package/dist/region.d.mts +24 -0
- package/dist/region.d.mts.map +1 -0
- package/dist/region.mjs +16 -0
- package/dist/region.mjs.map +1 -0
- package/dist/region.test.d.mts +1 -0
- package/dist/region.test.mjs +13656 -0
- package/dist/region.test.mjs.map +1 -0
- package/package.json +2 -2
- package/src/OpenAIEmbedding.ts +42 -27
- package/src/Responses.ts +3 -1
- package/src/codec.ts +8 -7
- package/src/index.ts +1 -0
- package/src/region.test.ts +26 -0
- package/src/region.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/responses",
|
|
3
|
-
"version": "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.
|
|
52
|
+
"@effect-uai/core": "0.7.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@effect-uai/core": ">=0.2.0 <1",
|
package/src/OpenAIEmbedding.ts
CHANGED
|
@@ -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,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
type EmbedResponse,
|
|
18
20
|
} from "@effect-uai/core/EmbeddingModel"
|
|
19
21
|
import type { OpenAIEmbeddingModel } from "./models.js"
|
|
22
|
+
import { type OpenAiRegion, resolveHost } from "./region.js"
|
|
20
23
|
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
22
25
|
// Public types
|
|
@@ -56,37 +59,36 @@ export class OpenAIEmbedding extends Context.Service<OpenAIEmbedding, OpenAIEmbe
|
|
|
56
59
|
export type Config = {
|
|
57
60
|
readonly apiKey: Redacted.Redacted
|
|
58
61
|
readonly baseUrl?: string
|
|
62
|
+
readonly region?: OpenAiRegion
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// ---------------------------------------------------------------------------
|
|
62
66
|
// Codec - input flattening to the single string OpenAI expects
|
|
63
67
|
// ---------------------------------------------------------------------------
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
})
|
|
71
78
|
|
|
72
|
-
const partToText = (
|
|
73
|
-
|
|
74
|
-
part: EmbedContentPart,
|
|
75
|
-
): Effect.Effect<string, AiError.AiError> =>
|
|
76
|
-
"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)
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* OpenAI accepts a single string per input. Pure-text `content[]` is
|
|
80
84
|
* concatenated with newlines (treats them as paragraphs of one document);
|
|
81
|
-
* any image part fails with `
|
|
85
|
+
* any image part fails with `Unsupported`.
|
|
82
86
|
*/
|
|
83
87
|
const inputToString = (input: EmbedInput): Effect.Effect<string, AiError.AiError> => {
|
|
84
88
|
if (typeof input === "string") return Effect.succeed(input)
|
|
85
89
|
if ("text" in input) return Effect.succeed(input.text)
|
|
86
|
-
if ("image" in input) return Effect.fail(imageRejected
|
|
87
|
-
return Effect.forEach(input.content, (
|
|
88
|
-
Effect.map((texts) => texts.join("\n")),
|
|
89
|
-
)
|
|
90
|
+
if ("image" in input) return Effect.fail(imageRejected)
|
|
91
|
+
return Effect.forEach(input.content, partToText).pipe(Effect.map((texts) => texts.join("\n")))
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
// ---------------------------------------------------------------------------
|
|
@@ -176,15 +178,13 @@ const transportFailure = (cause: unknown): AiError.AiError =>
|
|
|
176
178
|
// HTTP plumbing
|
|
177
179
|
// ---------------------------------------------------------------------------
|
|
178
180
|
|
|
179
|
-
const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.openai.com/v1"
|
|
180
|
-
|
|
181
181
|
const postEmbed = (
|
|
182
182
|
cfg: Config,
|
|
183
183
|
body: WireBody,
|
|
184
184
|
): Effect.Effect<typeof WireResponse.Type, AiError.AiError, HttpClient.HttpClient> =>
|
|
185
185
|
Effect.gen(function* () {
|
|
186
186
|
const client = yield* HttpClient.HttpClient
|
|
187
|
-
const httpRequest = HttpClientRequest.post(`${
|
|
187
|
+
const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/embeddings`).pipe(
|
|
188
188
|
HttpClientRequest.bearerToken(cfg.apiKey),
|
|
189
189
|
HttpClientRequest.bodyJsonUnsafe(body),
|
|
190
190
|
)
|
|
@@ -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
|
|
276
|
-
//
|
|
277
|
-
//
|
|
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
|
-
) =>
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
)
|
package/src/Responses.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as SSE from "@effect-uai/core/SSE"
|
|
|
12
12
|
import type { Turn, TurnEvent } from "@effect-uai/core/Turn"
|
|
13
13
|
import { itemsToInput } from "./codec.js"
|
|
14
14
|
import type { OpenAIModel } from "./models.js"
|
|
15
|
+
import { type OpenAiRegion, resolveHost } from "./region.js"
|
|
15
16
|
import {
|
|
16
17
|
KnownProviderEvent,
|
|
17
18
|
ProviderEvent,
|
|
@@ -95,6 +96,7 @@ export class Responses extends Context.Service<Responses, ResponsesService>()(
|
|
|
95
96
|
export type Config = {
|
|
96
97
|
readonly apiKey: Redacted.Redacted
|
|
97
98
|
readonly baseUrl?: string
|
|
99
|
+
readonly region?: OpenAiRegion
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// ---------------------------------------------------------------------------
|
|
@@ -222,7 +224,7 @@ const httpStatusError = (status: number, body: string): AiError.AiError => {
|
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
const buildNativeStream = (cfg: Config) => {
|
|
225
|
-
const url = `${cfg
|
|
227
|
+
const url = `${resolveHost(cfg)}/responses`
|
|
226
228
|
return (
|
|
227
229
|
request: ResponsesRequest,
|
|
228
230
|
): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>
|
package/src/codec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Encoding, Match, Option, Schema } from "effect"
|
|
2
|
-
import type { ContentBlock, InputImage,
|
|
2
|
+
import type { ContentBlock, InputImage, HistoryItem } from "@effect-uai/core/Items"
|
|
3
3
|
import type { Turn } from "@effect-uai/core/Turn"
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -137,7 +137,7 @@ export type WireResponseCompleted = typeof WireResponseCompleted.Type
|
|
|
137
137
|
* verbatim - preserves `encrypted_content`, item ids, and any field this
|
|
138
138
|
* codec doesn't model.
|
|
139
139
|
*/
|
|
140
|
-
const passthrough = (item:
|
|
140
|
+
const passthrough = (item: HistoryItem): Record<string, unknown> | undefined =>
|
|
141
141
|
item.providerData !== undefined &&
|
|
142
142
|
typeof item.providerData === "object" &&
|
|
143
143
|
item.providerData !== null
|
|
@@ -168,7 +168,7 @@ const contentBlockToInput = Match.type<ContentBlock>().pipe(
|
|
|
168
168
|
}),
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
-
const itemToInput = (item:
|
|
171
|
+
const itemToInput = (item: HistoryItem): Record<string, unknown> =>
|
|
172
172
|
passthrough(item) ??
|
|
173
173
|
Match.value(item).pipe(
|
|
174
174
|
Match.discriminatorsExhaustive("type")({
|
|
@@ -199,9 +199,10 @@ const itemToInput = (item: Item): Record<string, unknown> =>
|
|
|
199
199
|
}),
|
|
200
200
|
)
|
|
201
201
|
|
|
202
|
-
/** Convert our `
|
|
203
|
-
export const itemsToInput = (
|
|
204
|
-
items
|
|
202
|
+
/** Convert our `HistoryItem[]` history into the Responses API `input` array. */
|
|
203
|
+
export const itemsToInput = (
|
|
204
|
+
items: ReadonlyArray<HistoryItem>,
|
|
205
|
+
): ReadonlyArray<Record<string, unknown>> => items.map(itemToInput)
|
|
205
206
|
|
|
206
207
|
// ---------------------------------------------------------------------------
|
|
207
208
|
// Wire output items → our Items
|
|
@@ -218,7 +219,7 @@ const wireMessageContentToBlock = Match.type<typeof WireMessageContent.Type>().p
|
|
|
218
219
|
}),
|
|
219
220
|
)
|
|
220
221
|
|
|
221
|
-
export const wireItemToItem = (wire: WireOutputItem):
|
|
222
|
+
export const wireItemToItem = (wire: WireOutputItem): HistoryItem =>
|
|
222
223
|
Match.value(wire).pipe(
|
|
223
224
|
Match.discriminatorsExhaustive("type")({
|
|
224
225
|
message: (m) => ({
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { resolveHost } from "./region.js"
|
|
3
|
+
|
|
4
|
+
describe("responses/region.resolveHost", () => {
|
|
5
|
+
it("returns default host when no region or baseUrl", () => {
|
|
6
|
+
expect(resolveHost({})).toBe("https://api.openai.com/v1")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("returns default host for region 'default'", () => {
|
|
10
|
+
expect(resolveHost({ region: "default" })).toBe("https://api.openai.com/v1")
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("computes EU host for region 'eu'", () => {
|
|
14
|
+
expect(resolveHost({ region: "eu" })).toBe("https://eu.api.openai.com/v1")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("passes unknown region through as host prefix", () => {
|
|
18
|
+
expect(resolveHost({ region: "apac" })).toBe("https://apac.api.openai.com/v1")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("baseUrl wins over region", () => {
|
|
22
|
+
expect(resolveHost({ baseUrl: "http://localhost:8080/v1", region: "eu" })).toBe(
|
|
23
|
+
"http://localhost:8080/v1",
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
})
|
package/src/region.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI regional endpoints. `eu.api.openai.com` covers `/v1/responses`
|
|
3
|
+
* and `/v1/embeddings` (along with the rest of the REST surface) for
|
|
4
|
+
* projects with EU data residency enabled. Keys are project-scoped.
|
|
5
|
+
*
|
|
6
|
+
* Reference: https://developers.openai.com/api/docs/guides/your-data
|
|
7
|
+
*
|
|
8
|
+
* Defined locally in this package rather than imported from
|
|
9
|
+
* `@effect-uai/openai` to keep the packages decoupled.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
13
|
+
export type OpenAiRegion = "default" | "eu" | (string & {})
|
|
14
|
+
|
|
15
|
+
const DEFAULT_HOST = "https://api.openai.com/v1"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the HTTP base URL from a Config. `baseUrl` wins when set;
|
|
19
|
+
* otherwise the host is computed from `region`. Unknown region strings
|
|
20
|
+
* pass through as `{region}.api.openai.com/v1` for forward compat.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveHost = (cfg: {
|
|
23
|
+
readonly baseUrl?: string
|
|
24
|
+
readonly region?: OpenAiRegion
|
|
25
|
+
}): string => {
|
|
26
|
+
if (cfg.baseUrl !== undefined) return cfg.baseUrl
|
|
27
|
+
if (cfg.region === undefined || cfg.region === "default") return DEFAULT_HOST
|
|
28
|
+
return `https://${cfg.region}.api.openai.com/v1`
|
|
29
|
+
}
|