@effect-uai/responses 0.11.0 → 0.12.1

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 (49) hide show
  1. package/dist/OpenAIDeepResearch.d.mts +6 -5
  2. package/dist/OpenAIDeepResearch.d.mts.map +1 -1
  3. package/dist/OpenAIDeepResearch.mjs +8 -4
  4. package/dist/OpenAIDeepResearch.mjs.map +1 -1
  5. package/dist/OpenAIEmbedding.d.mts +2 -3
  6. package/dist/OpenAIEmbedding.d.mts.map +1 -1
  7. package/dist/OpenAIEmbedding.mjs +1 -1
  8. package/dist/OpenAIEmbedding.mjs.map +1 -1
  9. package/dist/Responses.d.mts +20 -2
  10. package/dist/Responses.d.mts.map +1 -1
  11. package/dist/Responses.mjs +9 -6
  12. package/dist/Responses.mjs.map +1 -1
  13. package/dist/ResponsesTools.d.mts +0 -2
  14. package/dist/ResponsesTools.d.mts.map +1 -1
  15. package/dist/ResponsesTools.mjs +1 -1
  16. package/dist/ResponsesTools.mjs.map +1 -1
  17. package/dist/codec.d.mts +0 -2
  18. package/dist/codec.d.mts.map +1 -1
  19. package/dist/codec.mjs +31 -9
  20. package/dist/codec.mjs.map +1 -1
  21. package/dist/codec.test.d.mts +1 -0
  22. package/dist/codec.test.mjs +94 -0
  23. package/dist/codec.test.mjs.map +1 -0
  24. package/dist/{magic-string.es-CaWgF-3p.mjs → magic-string.es-CUAnjKYC.mjs} +14 -13
  25. package/dist/{magic-string.es-CaWgF-3p.mjs.map → magic-string.es-CUAnjKYC.mjs.map} +1 -1
  26. package/dist/models.d.mts.map +1 -1
  27. package/dist/region.d.mts.map +1 -1
  28. package/dist/region.mjs.map +1 -1
  29. package/dist/region.test.d.mts +1 -1
  30. package/dist/region.test.mjs +1 -1
  31. package/dist/region.test.mjs.map +1 -1
  32. package/dist/responsesTools.test.d.mts +1 -1
  33. package/dist/responsesTools.test.mjs +7 -5
  34. package/dist/responsesTools.test.mjs.map +1 -1
  35. package/dist/streamEvents.d.mts +0 -2
  36. package/dist/streamEvents.d.mts.map +1 -1
  37. package/dist/streamEvents.mjs +1 -1
  38. package/dist/streamEvents.mjs.map +1 -1
  39. package/dist/{test.DNmyFkvJ-CPKukVWS.mjs → test.DNmyFkvJ-N3QqMRwZ.mjs} +247 -222
  40. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
  41. package/package.json +9 -5
  42. package/src/OpenAIDeepResearch.ts +9 -14
  43. package/src/OpenAIEmbedding.ts +24 -29
  44. package/src/Responses.ts +28 -8
  45. package/src/ResponsesTools.ts +4 -6
  46. package/src/codec.test.ts +87 -0
  47. package/src/codec.ts +33 -13
  48. package/dist/test.DNmyFkvJ-CPKukVWS.mjs.map +0 -1
  49. /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/responses",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "OpenAI Responses API provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/Responses.d.mts",
39
39
  "import": "./dist/Responses.mjs"
40
40
  },
41
+ "./ResponsesTools": {
42
+ "types": "./dist/ResponsesTools.d.mts",
43
+ "import": "./dist/ResponsesTools.mjs"
44
+ },
41
45
  "./OpenAIDeepResearch": {
42
46
  "types": "./dist/OpenAIDeepResearch.d.mts",
43
47
  "import": "./dist/OpenAIDeepResearch.mjs"
@@ -51,13 +55,13 @@
51
55
  "access": "public"
52
56
  },
53
57
  "devDependencies": {
54
- "effect": "4.0.0-beta.94",
55
- "typescript": "^6.0.3",
56
- "@effect-uai/core": "0.11.0"
58
+ "effect": "4.0.0-rc.111",
59
+ "typescript": "^7.0.2",
60
+ "@effect-uai/core": "0.12.1"
57
61
  },
58
62
  "peerDependencies": {
59
63
  "@effect-uai/core": ">=0.2.0 <1",
60
- "effect": ">=4.0.0-beta.94 <5.0.0"
64
+ "effect": ">=4.0.0-rc.111 <5.0.0"
61
65
  },
62
66
  "scripts": {
63
67
  "build": "tsdown",
@@ -87,20 +87,15 @@ export const jobStateOf = (wire: WireResponseCompleted): ResearchState =>
87
87
  Match.value(wire.status ?? "").pipe(
88
88
  Match.when("queued", (): ResearchState => ({ _tag: "Pending" })),
89
89
  Match.when("in_progress", (): ResearchState => ({ _tag: "Running" })),
90
- Match.whenOr(
91
- "completed",
92
- "incomplete",
93
- (): ResearchState => ({ _tag: "Succeeded", result: turnFromCompleted(wire) }),
94
- ),
95
- Match.whenOr(
96
- "failed",
97
- "cancelled",
98
- (): ResearchState => ({
99
- _tag: "Failed",
100
- ...(wire.error?.message != null && { reason: wire.error.message }),
101
- raw: wire,
102
- }),
103
- ),
90
+ Match.whenOr("completed", "incomplete", (): ResearchState => ({
91
+ _tag: "Succeeded",
92
+ result: turnFromCompleted(wire),
93
+ })),
94
+ Match.whenOr("failed", "cancelled", (): ResearchState => ({
95
+ _tag: "Failed",
96
+ ...(wire.error?.message != null && { reason: wire.error.message }),
97
+ raw: wire,
98
+ })),
104
99
  Match.orElse((): ResearchState => ({ _tag: "Running" })),
105
100
  )
106
101
 
@@ -241,12 +241,10 @@ const embedManyImpl =
241
241
  ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>
242
242
  buildBatchBody(request).pipe(
243
243
  Effect.flatMap((body) => postEmbed(cfg, body)),
244
- Effect.map(
245
- (decoded): EmbedManyResponse => ({
246
- embeddings: orderedEmbeddings(decoded.data),
247
- usage: usageOf(decoded.usage),
248
- }),
249
- ),
244
+ Effect.map((decoded): EmbedManyResponse => ({
245
+ embeddings: orderedEmbeddings(decoded.data),
246
+ usage: usageOf(decoded.usage),
247
+ })),
250
248
  )
251
249
 
252
250
  // ---------------------------------------------------------------------------
@@ -279,29 +277,26 @@ export const layer = (
279
277
  const typed = Layer.effect(OpenAIEmbedding, make(cfg))
280
278
  const generic = Layer.effect(
281
279
  EmbeddingModel,
282
- Effect.map(
283
- make(cfg),
284
- (s): EmbeddingModelService => ({
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).
289
- embed: <E extends EmbedEncoding | undefined = undefined>(
290
- req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
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>,
296
- embedMany: <E extends EmbedEncoding | undefined = undefined>(
297
- req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
298
- ) =>
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>,
303
- }),
304
- ),
280
+ Effect.map(make(cfg), (s): EmbeddingModelService => ({
281
+ // OpenAI emits float32 only and has no task parameter. Reject a
282
+ // non-float32 `encoding` (bucket 1, else a float32 vector comes back
283
+ // mislabeled as the requested type) and warn on a dropped `task`
284
+ // (bucket 2).
285
+ embed: <E extends EmbedEncoding | undefined = undefined>(
286
+ req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
287
+ ) =>
288
+ assertEncoding(req.encoding, ["float32"], "openai").pipe(
289
+ Effect.andThen(warnTaskDropped(req.task)),
290
+ Effect.andThen(s.embed(req as OpenAIEmbedRequest)),
291
+ ) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
292
+ embedMany: <E extends EmbedEncoding | undefined = undefined>(
293
+ req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
294
+ ) =>
295
+ assertEncoding(req.encoding, ["float32"], "openai").pipe(
296
+ Effect.andThen(warnTaskDropped(req.task)),
297
+ Effect.andThen(s.embedMany(req as OpenAIEmbedManyRequest)),
298
+ ) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
299
+ })),
305
300
  )
306
301
  return Layer.merge(typed, generic)
307
302
  }
package/src/Responses.ts CHANGED
@@ -51,6 +51,20 @@ export type ResponsesRequest = Omit<CommonRequest, "model"> & {
51
51
  readonly safetyIdentifier?: string
52
52
  readonly promptCacheKey?: string
53
53
  readonly truncation?: "auto" | "disabled"
54
+ /**
55
+ * Vendor-specific body fields merged into the request as-is.
56
+ *
57
+ * The Responses dialect is spoken by gateways as well as by OpenAI, and a
58
+ * gateway carries its own parameters — Requesty's `{ requesty: { auto_cache:
59
+ * true } }`, for example. Modelling each one would tie this provider to a
60
+ * catalogue of gateways it cannot see; passing them through keeps the typed
61
+ * surface OpenAI's and the untyped surface open.
62
+ *
63
+ * Merged over the built body, so a field named here wins over the one this
64
+ * provider would have sent. Per-request keys win over the layer's
65
+ * {@link Config.extraBody}.
66
+ */
67
+ readonly extraBody?: Readonly<Record<string, unknown>>
54
68
  /**
55
69
  * Free-form JSON output: model emits valid JSON without schema
56
70
  * constraints. Mutually exclusive with `structured` (schema wins if
@@ -108,6 +122,12 @@ export type Config = {
108
122
  readonly apiKey: Redacted.Redacted
109
123
  readonly baseUrl?: string
110
124
  readonly region?: OpenAiRegion
125
+ /**
126
+ * Vendor-specific body fields sent on every request from this layer — the
127
+ * place for a gateway's standing configuration, so call sites do not repeat
128
+ * it. Overridden per request by {@link ResponsesRequest.extraBody}.
129
+ */
130
+ readonly extraBody?: Readonly<Record<string, unknown>>
111
131
  }
112
132
 
113
133
  // ---------------------------------------------------------------------------
@@ -136,6 +156,7 @@ const buildText = (request: ResponsesRequest): Record<string, unknown> | undefin
136
156
  const buildBody = (
137
157
  request: ResponsesRequest,
138
158
  providerToolWire: ReadonlyArray<Record<string, unknown>>,
159
+ cfg: Config,
139
160
  ): Record<string, unknown> => {
140
161
  const text = buildText(request)
141
162
  const functionTools = descriptorsOf(request.tools).map((t) => ({
@@ -176,6 +197,8 @@ const buildBody = (
176
197
  }),
177
198
  ...(request.truncation !== undefined && { truncation: request.truncation }),
178
199
  ...(text !== undefined && { text }),
200
+ ...cfg.extraBody,
201
+ ...request.extraBody,
179
202
  }
180
203
  }
181
204
 
@@ -309,7 +332,7 @@ const buildNativeStream = (cfg: Config) => {
309
332
  )
310
333
  const httpRequest = HttpClientRequest.post(url).pipe(
311
334
  HttpClientRequest.bearerToken(cfg.apiKey),
312
- HttpClientRequest.bodyJsonUnsafe(buildBody(request, providerToolWire)),
335
+ HttpClientRequest.bodyJsonUnsafe(buildBody(request, providerToolWire, cfg)),
313
336
  HttpClientRequest.accept("text/event-stream"),
314
337
  )
315
338
  const response = yield* client
@@ -373,13 +396,10 @@ export const layer = (
373
396
  const typed = Layer.effect(Responses, make(cfg))
374
397
  const generic = Layer.effect(
375
398
  LanguageModel,
376
- Effect.map(
377
- make(cfg),
378
- (s): LanguageModelService => ({
379
- streamTurn: (request) => s.streamTurn(request as ResponsesRequest),
380
- turn: (request) => s.turn(request as ResponsesRequest),
381
- }),
382
- ),
399
+ Effect.map(make(cfg), (s): LanguageModelService => ({
400
+ streamTurn: (request) => s.streamTurn(request as ResponsesRequest),
401
+ turn: (request) => s.turn(request as ResponsesRequest),
402
+ })),
383
403
  )
384
404
  return Layer.merge(typed, generic)
385
405
  }
@@ -139,12 +139,10 @@ const renderOne = (
139
139
  })
140
140
  : decodeConfig(tool.config).pipe(
141
141
  Result.map(configToWire),
142
- Result.mapError(
143
- (): ToolRenderError => ({
144
- capability: "providerTool",
145
- reason: `unrecognized Responses provider tool config on "${tool.name}"`,
146
- }),
147
- ),
142
+ Result.mapError((): ToolRenderError => ({
143
+ capability: "providerTool",
144
+ reason: `unrecognized Responses provider tool config on "${tool.name}"`,
145
+ })),
148
146
  )
149
147
 
150
148
  /**
@@ -0,0 +1,87 @@
1
+ import type { HistoryItem } from "@effect-uai/core/Items"
2
+ import { describe, expect, it } from "vitest"
3
+ import { itemsToInput, wireItemToItem } from "./codec.js"
4
+
5
+ describe("providerData round-trip", () => {
6
+ it("re-emits a stashed item verbatim, preserving fields our shape drops", () => {
7
+ // `id` and `encrypted_content` cannot be reconstructed from a Reasoning
8
+ // item, so the round-trip has to go through the stash.
9
+ const items = wireItemToItem({
10
+ type: "reasoning",
11
+ id: "rs_1",
12
+ encrypted_content: "opaque-state",
13
+ })
14
+ expect(itemsToInput(items)).toEqual([
15
+ { type: "reasoning", id: "rs_1", encrypted_content: "opaque-state" },
16
+ ])
17
+ })
18
+ })
19
+
20
+ describe("providerData is a shared slot", () => {
21
+ // Regression: the passthrough used to re-emit the whole `providerData`
22
+ // object as the wire item. An item that had been through another provider
23
+ // first was sent as that provider's data, losing the real content.
24
+ it("ignores another provider's data and encodes the item normally", () => {
25
+ const history: ReadonlyArray<HistoryItem> = [
26
+ {
27
+ type: "message",
28
+ role: "user",
29
+ content: [{ type: "input_text", text: "hello world" }],
30
+ providerData: { gemini: { thoughtSignature: "sig-abc" } },
31
+ },
32
+ ]
33
+ expect(itemsToInput(history)).toEqual([
34
+ { type: "message", role: "user", content: [{ type: "input_text", text: "hello world" }] },
35
+ ])
36
+ })
37
+
38
+ it("does not send a foreign function_call as junk", () => {
39
+ const history: ReadonlyArray<HistoryItem> = [
40
+ {
41
+ type: "function_call",
42
+ call_id: "c1",
43
+ name: "get_weather",
44
+ arguments: '{"city":"Lisbon"}',
45
+ providerData: { gemini: { id: "g1", thoughtSignature: "sig-abc" } },
46
+ },
47
+ ]
48
+ expect(itemsToInput(history)).toEqual([
49
+ {
50
+ type: "function_call",
51
+ call_id: "c1",
52
+ name: "get_weather",
53
+ arguments: '{"city":"Lisbon"}',
54
+ },
55
+ ])
56
+ })
57
+
58
+ it("re-emits only our key when an item carries several providers' data", () => {
59
+ const history: ReadonlyArray<HistoryItem> = [
60
+ {
61
+ type: "reasoning",
62
+ signature: "opaque-state",
63
+ providerData: {
64
+ responses: { type: "reasoning", id: "rs_1", encrypted_content: "opaque-state" },
65
+ gemini: { thoughtSignature: "sig-abc" },
66
+ },
67
+ },
68
+ ]
69
+ expect(itemsToInput(history)).toEqual([
70
+ { type: "reasoning", id: "rs_1", encrypted_content: "opaque-state" },
71
+ ])
72
+ })
73
+
74
+ it("falls back to a normal encode when the stashed value is not one of ours", () => {
75
+ const history: ReadonlyArray<HistoryItem> = [
76
+ {
77
+ type: "message",
78
+ role: "user",
79
+ content: [{ type: "input_text", text: "hi" }],
80
+ providerData: { responses: { type: "not_an_item_we_model" } },
81
+ },
82
+ ]
83
+ expect(itemsToInput(history)).toEqual([
84
+ { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
85
+ ])
86
+ })
87
+ })
package/src/codec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Encoding, Match, Option, Schema } from "effect"
1
+ import { Encoding, Match, Option, Result, Schema, pipe } from "effect"
2
2
  import type { ContentBlock, InputImage, HistoryItem } from "@effect-uai/core/Items"
3
3
  import type { Turn } from "@effect-uai/core/Turn"
4
4
 
@@ -159,16 +159,36 @@ export type WireResponseCompleted = typeof WireResponseCompleted.Type
159
159
  // ---------------------------------------------------------------------------
160
160
 
161
161
  /**
162
- * If an item carries `providerData` from a prior OpenAI turn, re-emit it
163
- * verbatim - preserves `encrypted_content`, item ids, and any field this
164
- * codec doesn't model.
162
+ * The item shapes we stash for round-tripping. Narrower than
163
+ * `WireOutputItem`: the provider-hosted call variants are dropped from the
164
+ * turn, so they are never stored. Composed from the internal `Wire*` structs,
165
+ * which keeps the wire model out of the package's public surface.
165
166
  */
166
- const passthrough = (item: HistoryItem): Record<string, unknown> | undefined =>
167
- item.providerData !== undefined &&
168
- typeof item.providerData === "object" &&
169
- item.providerData !== null
170
- ? (item.providerData as Record<string, unknown>)
171
- : undefined
167
+ const StoredItem = Schema.Union([WireMessage, WireFunctionCall, WireReasoning])
168
+ type StoredItem = typeof StoredItem.Type
169
+
170
+ const ProviderDataResponses = Schema.Struct({ responses: StoredItem })
171
+ const decodeResponsesData = Schema.decodeUnknownResult(ProviderDataResponses)
172
+
173
+ /**
174
+ * If an item carries `providerData.responses` from a prior turn on this API,
175
+ * re-emit it verbatim - preserves `encrypted_content` and item ids, which a
176
+ * fresh encode from our `HistoryItem` shape cannot reconstruct.
177
+ *
178
+ * Namespaced because `providerData` is a shared slot: an item routed through
179
+ * another provider first (dynamic fallback) carries that provider's key, and
180
+ * re-emitting the whole object would send that provider's data as the wire
181
+ * item and drop the real content. Anything that fails to decode is not ours,
182
+ * so we fall back to encoding the item normally.
183
+ */
184
+ const passthrough = (item: HistoryItem): StoredItem | undefined =>
185
+ pipe(
186
+ decodeResponsesData(item.providerData),
187
+ Result.match({
188
+ onSuccess: (data) => data.responses,
189
+ onFailure: (): StoredItem | undefined => undefined,
190
+ }),
191
+ )
172
192
 
173
193
  /**
174
194
  * OpenAI's `input_image` content block carries a single `image_url` field;
@@ -253,7 +273,7 @@ export const wireItemToItem = (wire: WireOutputItem): ReadonlyArray<HistoryItem>
253
273
  type: "message" as const,
254
274
  role: m.role,
255
275
  content: m.content.map(wireMessageContentToBlock),
256
- providerData: m,
276
+ providerData: { responses: m },
257
277
  },
258
278
  ],
259
279
  function_call: (f): ReadonlyArray<HistoryItem> => [
@@ -262,7 +282,7 @@ export const wireItemToItem = (wire: WireOutputItem): ReadonlyArray<HistoryItem>
262
282
  call_id: f.call_id,
263
283
  name: f.name,
264
284
  arguments: f.arguments,
265
- providerData: f,
285
+ providerData: { responses: f },
266
286
  },
267
287
  ],
268
288
  reasoning: (r): ReadonlyArray<HistoryItem> => [
@@ -273,7 +293,7 @@ export const wireItemToItem = (wire: WireOutputItem): ReadonlyArray<HistoryItem>
273
293
  summary: r.summary.map((s) => s.text).join("\n"),
274
294
  }),
275
295
  ...(r.encrypted_content !== undefined && { signature: r.encrypted_content }),
276
- providerData: r,
296
+ providerData: { responses: r },
277
297
  },
278
298
  ],
279
299
  // Hosted-tool calls are provider-executed; drop from the turn (Phase 1).