@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.
- package/dist/OpenAIDeepResearch.d.mts +54 -0
- package/dist/OpenAIDeepResearch.d.mts.map +1 -0
- package/dist/OpenAIDeepResearch.mjs +121 -0
- package/dist/OpenAIDeepResearch.mjs.map +1 -0
- package/dist/OpenAIEmbedding.mjs +1 -1
- package/dist/OpenAIEmbedding.mjs.map +1 -1
- package/dist/Responses.d.mts +13 -3
- package/dist/Responses.d.mts.map +1 -1
- package/dist/Responses.mjs +68 -33
- package/dist/Responses.mjs.map +1 -1
- package/dist/ResponsesTools.d.mts +85 -0
- package/dist/ResponsesTools.d.mts.map +1 -0
- package/dist/ResponsesTools.mjs +126 -0
- package/dist/ResponsesTools.mjs.map +1 -0
- package/dist/codec.d.mts +42 -3
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +28 -9
- package/dist/codec.mjs.map +1 -1
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +5 -3
- package/dist/{magic-string.es-PAZgLX4h.mjs → magic-string.es-CaWgF-3p.mjs} +1 -1
- package/dist/{magic-string.es-PAZgLX4h.mjs.map → magic-string.es-CaWgF-3p.mjs.map} +1 -1
- package/dist/models.d.mts +10 -1
- package/dist/models.d.mts.map +1 -1
- package/dist/region.test.mjs +1 -13630
- package/dist/region.test.mjs.map +1 -1
- package/dist/responsesTools.test.d.mts +1 -0
- package/dist/responsesTools.test.mjs +138 -0
- package/dist/responsesTools.test.mjs.map +1 -0
- package/dist/streamEvents.d.mts +142 -0
- package/dist/streamEvents.d.mts.map +1 -1
- package/dist/streamEvents.mjs +27 -2
- package/dist/streamEvents.mjs.map +1 -1
- package/dist/test.DNmyFkvJ-CPKukVWS.mjs +13633 -0
- package/dist/test.DNmyFkvJ-CPKukVWS.mjs.map +1 -0
- package/package.json +8 -4
- package/src/OpenAIDeepResearch.ts +266 -0
- package/src/OpenAIEmbedding.ts +1 -1
- package/src/Responses.ts +94 -36
- package/src/ResponsesTools.ts +158 -0
- package/src/codec.ts +62 -26
- package/src/index.ts +2 -0
- package/src/models.ts +14 -0
- package/src/responsesTools.test.ts +146 -0
- 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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 ?? []).
|
|
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
|
+
})
|
package/src/streamEvents.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Match, Schema } from "effect"
|
|
2
2
|
import { TurnEvent } from "@effect-uai/core/Turn"
|
|
3
|
-
import {
|
|
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
|
],
|