@effect-uai/responses 0.9.0 → 0.11.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.
Files changed (45) hide show
  1. package/dist/OpenAIDeepResearch.d.mts +54 -0
  2. package/dist/OpenAIDeepResearch.d.mts.map +1 -0
  3. package/dist/OpenAIDeepResearch.mjs +121 -0
  4. package/dist/OpenAIDeepResearch.mjs.map +1 -0
  5. package/dist/OpenAIEmbedding.mjs +1 -1
  6. package/dist/OpenAIEmbedding.mjs.map +1 -1
  7. package/dist/Responses.d.mts +13 -3
  8. package/dist/Responses.d.mts.map +1 -1
  9. package/dist/Responses.mjs +68 -33
  10. package/dist/Responses.mjs.map +1 -1
  11. package/dist/ResponsesTools.d.mts +85 -0
  12. package/dist/ResponsesTools.d.mts.map +1 -0
  13. package/dist/ResponsesTools.mjs +126 -0
  14. package/dist/ResponsesTools.mjs.map +1 -0
  15. package/dist/codec.d.mts +42 -3
  16. package/dist/codec.d.mts.map +1 -1
  17. package/dist/codec.mjs +28 -9
  18. package/dist/codec.mjs.map +1 -1
  19. package/dist/index.d.mts +5 -3
  20. package/dist/index.mjs +5 -3
  21. package/dist/{magic-string.es-PAZgLX4h.mjs → magic-string.es-CaWgF-3p.mjs} +1 -1
  22. package/dist/{magic-string.es-PAZgLX4h.mjs.map → magic-string.es-CaWgF-3p.mjs.map} +1 -1
  23. package/dist/models.d.mts +10 -1
  24. package/dist/models.d.mts.map +1 -1
  25. package/dist/region.test.mjs +1 -13630
  26. package/dist/region.test.mjs.map +1 -1
  27. package/dist/responsesTools.test.d.mts +1 -0
  28. package/dist/responsesTools.test.mjs +138 -0
  29. package/dist/responsesTools.test.mjs.map +1 -0
  30. package/dist/streamEvents.d.mts +142 -0
  31. package/dist/streamEvents.d.mts.map +1 -1
  32. package/dist/streamEvents.mjs +27 -2
  33. package/dist/streamEvents.mjs.map +1 -1
  34. package/dist/test.DNmyFkvJ-CPKukVWS.mjs +13633 -0
  35. package/dist/test.DNmyFkvJ-CPKukVWS.mjs.map +1 -0
  36. package/package.json +8 -4
  37. package/src/OpenAIDeepResearch.ts +266 -0
  38. package/src/OpenAIEmbedding.ts +1 -1
  39. package/src/Responses.ts +94 -36
  40. package/src/ResponsesTools.ts +158 -0
  41. package/src/codec.ts +62 -26
  42. package/src/index.ts +2 -0
  43. package/src/models.ts +14 -0
  44. package/src/responsesTools.test.ts +146 -0
  45. package/src/streamEvents.ts +58 -1
package/src/codec.ts CHANGED
@@ -35,7 +35,7 @@ const WireFilePath = Schema.Struct({
35
35
  index: Schema.Number,
36
36
  })
37
37
 
38
- const WireAnnotation = Schema.Union([
38
+ export const WireAnnotation = Schema.Union([
39
39
  WireUrlCitation,
40
40
  WireFileCitation,
41
41
  WireContainerFileCitation,
@@ -82,7 +82,33 @@ const WireReasoning = Schema.Struct({
82
82
  encrypted_content: Schema.optional(Schema.String),
83
83
  })
84
84
 
85
- export const WireOutputItem = Schema.Union([WireMessage, WireFunctionCall, WireReasoning])
85
+ // Provider-hosted tool call items (web search, code interpreter, file search).
86
+ // The provider executes them, not us — we model them only so a grounded
87
+ // `response.completed` still decodes and assembles a Turn; they are dropped
88
+ // from the turn's items in Phase 1. Surfacing them is a follow-up.
89
+ const WireWebSearchCall = Schema.Struct({
90
+ type: Schema.Literal("web_search_call"),
91
+ id: Schema.optional(Schema.String),
92
+ })
93
+
94
+ const WireCodeInterpreterCall = Schema.Struct({
95
+ type: Schema.Literal("code_interpreter_call"),
96
+ id: Schema.optional(Schema.String),
97
+ })
98
+
99
+ const WireFileSearchCall = Schema.Struct({
100
+ type: Schema.Literal("file_search_call"),
101
+ id: Schema.optional(Schema.String),
102
+ })
103
+
104
+ export const WireOutputItem = Schema.Union([
105
+ WireMessage,
106
+ WireFunctionCall,
107
+ WireReasoning,
108
+ WireWebSearchCall,
109
+ WireCodeInterpreterCall,
110
+ WireFileSearchCall,
111
+ ])
86
112
  export type WireOutputItem = typeof WireOutputItem.Type
87
113
 
88
114
  const WireInputTokensDetails = Schema.Struct({
@@ -219,31 +245,41 @@ const wireMessageContentToBlock = Match.type<typeof WireMessageContent.Type>().p
219
245
  }),
220
246
  )
221
247
 
222
- export const wireItemToItem = (wire: WireOutputItem): HistoryItem =>
248
+ export const wireItemToItem = (wire: WireOutputItem): ReadonlyArray<HistoryItem> =>
223
249
  Match.value(wire).pipe(
224
250
  Match.discriminatorsExhaustive("type")({
225
- message: (m) => ({
226
- type: "message" as const,
227
- role: m.role,
228
- content: m.content.map(wireMessageContentToBlock),
229
- providerData: m,
230
- }),
231
- function_call: (f) => ({
232
- type: "function_call" as const,
233
- call_id: f.call_id,
234
- name: f.name,
235
- arguments: f.arguments,
236
- providerData: f,
237
- }),
238
- reasoning: (r) => ({
239
- type: "reasoning" as const,
240
- ...(r.id !== undefined && { id: r.id }),
241
- ...(r.summary !== undefined && {
242
- summary: r.summary.map((s) => s.text).join("\n"),
243
- }),
244
- ...(r.encrypted_content !== undefined && { signature: r.encrypted_content }),
245
- providerData: r,
246
- }),
251
+ message: (m): ReadonlyArray<HistoryItem> => [
252
+ {
253
+ type: "message" as const,
254
+ role: m.role,
255
+ content: m.content.map(wireMessageContentToBlock),
256
+ providerData: m,
257
+ },
258
+ ],
259
+ function_call: (f): ReadonlyArray<HistoryItem> => [
260
+ {
261
+ type: "function_call" as const,
262
+ call_id: f.call_id,
263
+ name: f.name,
264
+ arguments: f.arguments,
265
+ providerData: f,
266
+ },
267
+ ],
268
+ reasoning: (r): ReadonlyArray<HistoryItem> => [
269
+ {
270
+ type: "reasoning" as const,
271
+ ...(r.id !== undefined && { id: r.id }),
272
+ ...(r.summary !== undefined && {
273
+ summary: r.summary.map((s) => s.text).join("\n"),
274
+ }),
275
+ ...(r.encrypted_content !== undefined && { signature: r.encrypted_content }),
276
+ providerData: r,
277
+ },
278
+ ],
279
+ // Hosted-tool calls are provider-executed; drop from the turn (Phase 1).
280
+ web_search_call: (): ReadonlyArray<HistoryItem> => [],
281
+ code_interpreter_call: (): ReadonlyArray<HistoryItem> => [],
282
+ file_search_call: (): ReadonlyArray<HistoryItem> => [],
247
283
  }),
248
284
  )
249
285
 
@@ -285,7 +321,7 @@ export const turnFromCompleted = (payload: WireResponseCompleted): Turn => {
285
321
  const cached = cachedTokens(payload)
286
322
  const reasoning = reasoningTokens(payload)
287
323
  return {
288
- items: (payload.output ?? []).map(wireItemToItem),
324
+ items: (payload.output ?? []).flatMap(wireItemToItem),
289
325
  usage: {
290
326
  ...(payload.usage?.input_tokens !== undefined && {
291
327
  input_tokens: payload.usage.input_tokens,
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * as Responses from "./Responses.js"
2
+ export * as ResponsesTools from "./ResponsesTools.js"
3
+ export * as OpenAIDeepResearch from "./OpenAIDeepResearch.js"
2
4
  export * as OpenAIEmbedding from "./OpenAIEmbedding.js"
3
5
  export * as codec from "./codec.js"
4
6
  export * as streamEvents from "./streamEvents.js"
package/src/models.ts CHANGED
@@ -23,6 +23,20 @@ export type OpenAIModel =
23
23
  // eslint-disable-next-line @typescript-eslint/ban-types
24
24
  | (string & {})
25
25
 
26
+ /**
27
+ * OpenAI deep-research model / agent ids, usable via the Responses API with
28
+ * `background: true`. `o3-deep-research` is the comprehensive tier;
29
+ * `o4-mini-deep-research` is faster and cheaper. The `(string & {})` tail keeps
30
+ * autocomplete while accepting newer ids without an SDK update.
31
+ *
32
+ * Reference: https://developers.openai.com/api/docs/guides/deep-research
33
+ */
34
+ export type OpenAIDeepResearchModel =
35
+ | "o3-deep-research"
36
+ | "o4-mini-deep-research"
37
+ // eslint-disable-next-line @typescript-eslint/ban-types
38
+ | (string & {})
39
+
26
40
  /**
27
41
  * Known OpenAI embedding model identifiers. The `-3-` line is still the
28
42
  * current general-purpose lineup; `dimensions` parameter supports
@@ -0,0 +1,146 @@
1
+ import { Result, Schema } from "effect"
2
+ import * as Tool from "@effect-uai/core/Tool"
3
+ import { providerToolsOf } from "@effect-uai/core/Tool"
4
+ import { describe, expect, it } from "vitest"
5
+ import { KnownProviderEvent, eventToDeltas, makeCallIdLookup } from "./streamEvents.js"
6
+ import {
7
+ codeInterpreterTool,
8
+ fileSearchTool,
9
+ renderProviderTools,
10
+ webSearchTool,
11
+ } from "./ResponsesTools.js"
12
+
13
+ const wireFor = (toolkit: Record<string, Tool.AnyTool>) =>
14
+ Result.getOrThrow(renderProviderTools(providerToolsOf(toolkit)))
15
+
16
+ describe("Responses provider tool rendering", () => {
17
+ it("maps web_search options onto the native wire fields", () => {
18
+ expect(
19
+ wireFor({
20
+ web_search: webSearchTool({
21
+ searchContextSize: "high",
22
+ filters: { allowedDomains: ["example.com"], blockedDomains: ["spam.com"] },
23
+ userLocation: { city: "Berlin", country: "DE" },
24
+ }),
25
+ }),
26
+ ).toEqual([
27
+ {
28
+ type: "web_search",
29
+ filters: { allowed_domains: ["example.com"], blocked_domains: ["spam.com"] },
30
+ user_location: { type: "approximate", city: "Berlin", country: "DE" },
31
+ search_context_size: "high",
32
+ },
33
+ ])
34
+ })
35
+
36
+ it("maps code_interpreter to an auto container (with file ids when given)", () => {
37
+ expect(wireFor({ ci: codeInterpreterTool() })).toEqual([
38
+ { type: "code_interpreter", container: { type: "auto" } },
39
+ ])
40
+ expect(wireFor({ ci: codeInterpreterTool({ fileIds: ["file_1"] }) })).toEqual([
41
+ { type: "code_interpreter", container: { type: "auto", file_ids: ["file_1"] } },
42
+ ])
43
+ })
44
+
45
+ it("maps file_search to its vector store ids", () => {
46
+ expect(wireFor({ fs: fileSearchTool({ vectorStoreIds: ["vs_1", "vs_2"] }) })).toEqual([
47
+ { type: "file_search", vector_store_ids: ["vs_1", "vs_2"] },
48
+ ])
49
+ })
50
+
51
+ it("fails on a tool hosted by a different provider", () => {
52
+ const foreign = Tool.provider({
53
+ name: "google_search",
54
+ description: "Gemini grounding.",
55
+ inputSchema: Tool.noInput,
56
+ provider: "gemini",
57
+ config: { kind: "google_search" },
58
+ })
59
+ expect(Result.isFailure(renderProviderTools(providerToolsOf({ foreign })))).toBe(true)
60
+ })
61
+
62
+ it("fails on an unrecognized config", () => {
63
+ const bogus = Tool.provider({
64
+ name: "mystery",
65
+ description: "Unknown Responses tool.",
66
+ inputSchema: Tool.noInput,
67
+ provider: "openai",
68
+ config: { kind: "telepathy" },
69
+ })
70
+ expect(Result.isFailure(renderProviderTools(providerToolsOf({ bogus })))).toBe(true)
71
+ })
72
+ })
73
+
74
+ describe("Responses hosted-tool response tolerance", () => {
75
+ // The real regression: a hosted-tool output item in `response.completed` must
76
+ // not break the event decode, or `TurnComplete` would never fire. Decoding
77
+ // through the actual event schema (throws on failure) and projecting to
78
+ // TurnEvents proves the grounded text still assembles into a turn.
79
+ const decodeEvent = Schema.decodeUnknownSync(KnownProviderEvent)
80
+
81
+ it("emits TurnComplete with just the text answer for a grounded completion", () => {
82
+ const event = decodeEvent({
83
+ type: "response.completed",
84
+ response: {
85
+ status: "completed",
86
+ output: [
87
+ { type: "web_search_call", id: "ws_1", status: "completed" },
88
+ {
89
+ type: "message",
90
+ role: "assistant",
91
+ content: [{ type: "output_text", text: "Grounded answer." }],
92
+ },
93
+ ],
94
+ },
95
+ })
96
+ const deltas = eventToDeltas(event, makeCallIdLookup())
97
+ const complete = deltas.find((d) => d._tag === "TurnComplete")
98
+ expect(complete).toBeDefined()
99
+ const turn = complete?._tag === "TurnComplete" ? complete.turn : undefined
100
+ expect(turn?.stop_reason).toBe("stop")
101
+ expect(turn?.items).toEqual([
102
+ {
103
+ type: "message",
104
+ role: "assistant",
105
+ content: [{ type: "output_text", text: "Grounded answer." }],
106
+ providerData: expect.anything(),
107
+ },
108
+ ])
109
+ })
110
+
111
+ // The new streamed-citation path: a real `annotation.added` payload (with the
112
+ // wire's positional fields) must decode and project to a `CitationAdded`
113
+ // carrying a domain `Annotation`. This is what makes native-grounding
114
+ // citations stream instead of only landing on the final turn.
115
+ it("projects a streamed url_citation to CitationAdded", () => {
116
+ const deltas = eventToDeltas(
117
+ decodeEvent({
118
+ type: "response.output_text.annotation.added",
119
+ item_id: "msg_1",
120
+ output_index: 0,
121
+ content_index: 0,
122
+ annotation_index: 0,
123
+ annotation: {
124
+ type: "url_citation",
125
+ url: "https://example.com/a",
126
+ title: "A",
127
+ start_index: 10,
128
+ end_index: 20,
129
+ },
130
+ }),
131
+ makeCallIdLookup(),
132
+ )
133
+ expect(deltas).toEqual([
134
+ {
135
+ _tag: "CitationAdded",
136
+ annotation: {
137
+ type: "url_citation",
138
+ url: "https://example.com/a",
139
+ title: "A",
140
+ start_index: 10,
141
+ end_index: 20,
142
+ },
143
+ },
144
+ ])
145
+ })
146
+ })
@@ -1,6 +1,11 @@
1
1
  import { Match, Schema } from "effect"
2
2
  import { TurnEvent } from "@effect-uai/core/Turn"
3
- import { WireOutputItem, WireResponseCompleted, turnFromCompleted } from "./codec.js"
3
+ import {
4
+ WireAnnotation,
5
+ WireOutputItem,
6
+ WireResponseCompleted,
7
+ turnFromCompleted,
8
+ } from "./codec.js"
4
9
 
5
10
  // ---------------------------------------------------------------------------
6
11
  // Schemas for the SSE event payloads we care about. The Responses API ships
@@ -44,6 +49,28 @@ const RefusalDone = Schema.Struct({
44
49
  refusal: Schema.optional(Schema.String),
45
50
  })
46
51
 
52
+ // Web-search lifecycle. The three phases carry `item_id` / `output_index` /
53
+ // `sequence_number`; we map each to a `WebSearchCall` status. `query` / `action`
54
+ // ride on the `web_search_call` output item, not these lifecycle events.
55
+ const WebSearchInProgress = Schema.Struct({
56
+ type: Schema.Literal("response.web_search_call.in_progress"),
57
+ })
58
+
59
+ const WebSearchSearching = Schema.Struct({
60
+ type: Schema.Literal("response.web_search_call.searching"),
61
+ })
62
+
63
+ const WebSearchCompleted = Schema.Struct({
64
+ type: Schema.Literal("response.web_search_call.completed"),
65
+ })
66
+
67
+ // A citation attached to the answer as it is written. `annotation` is the
68
+ // same shape as the ones bundled on the final `output_text` block.
69
+ const AnnotationAdded = Schema.Struct({
70
+ type: Schema.Literal("response.output_text.annotation.added"),
71
+ annotation: WireAnnotation,
72
+ })
73
+
47
74
  const Completed = Schema.Struct({
48
75
  type: Schema.Literal("response.completed"),
49
76
  response: WireResponseCompleted,
@@ -61,8 +88,18 @@ const Failed = Schema.Struct({
61
88
 
62
89
  const ErrorEvent = Schema.Struct({
63
90
  type: Schema.Literal("error"),
91
+ // Flat form (top-level `message`/`code`) and the nested form the Responses API
92
+ // uses for request rejections: `error: { message, code, type, param }`.
64
93
  message: Schema.optional(Schema.String),
65
94
  code: Schema.optional(Schema.String),
95
+ error: Schema.optional(
96
+ Schema.Struct({
97
+ message: Schema.optional(Schema.NullOr(Schema.String)),
98
+ code: Schema.optional(Schema.NullOr(Schema.String)),
99
+ type: Schema.optional(Schema.NullOr(Schema.String)),
100
+ param: Schema.optional(Schema.NullOr(Schema.String)),
101
+ }),
102
+ ),
66
103
  })
67
104
 
68
105
  /**
@@ -88,6 +125,10 @@ export const KnownProviderEvent = Schema.Union([
88
125
  ReasoningSummaryDelta,
89
126
  RefusalDelta,
90
127
  RefusalDone,
128
+ WebSearchInProgress,
129
+ WebSearchSearching,
130
+ WebSearchCompleted,
131
+ AnnotationAdded,
91
132
  Completed,
92
133
  Incomplete,
93
134
  Failed,
@@ -107,6 +148,10 @@ export const ProviderEvent = Schema.Union([
107
148
  ReasoningSummaryDelta,
108
149
  RefusalDelta,
109
150
  RefusalDone,
151
+ WebSearchInProgress,
152
+ WebSearchSearching,
153
+ WebSearchCompleted,
154
+ AnnotationAdded,
110
155
  Completed,
111
156
  Incomplete,
112
157
  Failed,
@@ -165,6 +210,18 @@ export const eventToDeltas = (
165
210
  // Terminal marker for the refusal stream; the deltas already covered
166
211
  // the text, so no canonical event is emitted here.
167
212
  "response.refusal.done": () => [],
213
+ "response.web_search_call.in_progress": () => [
214
+ TurnEvent.WebSearchCall({ status: "started" }),
215
+ ],
216
+ "response.web_search_call.searching": () => [
217
+ TurnEvent.WebSearchCall({ status: "searching" }),
218
+ ],
219
+ "response.web_search_call.completed": () => [
220
+ TurnEvent.WebSearchCall({ status: "completed" }),
221
+ ],
222
+ "response.output_text.annotation.added": ({ annotation }) => [
223
+ TurnEvent.CitationAdded({ annotation }),
224
+ ],
168
225
  "response.completed": ({ response }) => [
169
226
  TurnEvent.TurnComplete({ turn: turnFromCompleted(response) }),
170
227
  ],