@robota-sdk/agent-provider-openai 3.0.0-beta.63 → 3.0.0-beta.82
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/LICENSE +661 -21
- package/dist/node/index.cjs +1 -2
- package/dist/node/index.d.cts +271 -267
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +271 -267
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -2
- package/dist/node/index.js.map +1 -0
- package/dist/node/loggers/index.cjs +1 -0
- package/dist/node/loggers/index.d.cts +94 -0
- package/dist/node/loggers/index.d.cts.map +1 -0
- package/dist/node/loggers/index.d.ts +94 -0
- package/dist/node/loggers/index.d.ts.map +1 -0
- package/dist/node/loggers/index.js +2 -0
- package/dist/node/loggers/index.js.map +1 -0
- package/dist/node/payload-logger-BaW0K8yI.d.cts +61 -0
- package/dist/node/payload-logger-BaW0K8yI.d.cts.map +1 -0
- package/dist/node/payload-logger-BaW0K8yI.d.ts +61 -0
- package/dist/node/payload-logger-BaW0K8yI.d.ts.map +1 -0
- package/dist/node/rolldown-runtime-CMqjfN_6.cjs +1 -0
- package/package.json +69 -86
- package/src/index.ts +1 -0
- package/src/openai/__tests__/endpoint-provenance.test.ts +44 -0
- package/src/openai/__tests__/provider-errors.test.ts +114 -0
- package/src/openai/__tests__/request-format.test.ts +59 -0
- package/src/openai/__tests__/strict-tools-closure.test.ts +136 -0
- package/src/openai/__tests__/tool-schema-projection.test.ts +302 -0
- package/src/openai/__tests__/trace-context-wire.test.ts +114 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +225 -0
- package/src/openai/executor-integration.test.ts +213 -0
- package/src/openai/index.ts +19 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +96 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +243 -0
- package/src/openai/loggers/file-payload-logger.ts +124 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +23 -0
- package/src/openai/model-effort-table.ts +19 -0
- package/src/openai/model-effort-verification-config.test.ts +45 -0
- package/src/openai/model-effort-verification-config.ts +35 -0
- package/src/openai/openai-request-format.ts +116 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +48 -0
- package/src/openai/provider-definition.test.ts +74 -0
- package/src/openai/provider-definition.ts +139 -0
- package/src/openai/provider.test.ts +1686 -0
- package/src/openai/provider.ts +392 -0
- package/src/openai/reasoning-effort.test.ts +272 -0
- package/src/openai/reasoning-effort.ts +49 -0
- package/src/openai/request-id.ts +39 -0
- package/src/openai/request-options.ts +14 -0
- package/src/openai/responses-chat.ts +285 -0
- package/src/openai/responses-converter.ts +122 -0
- package/src/openai/responses-parser.ts +297 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +193 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/types/api-types.ts +113 -0
- package/src/openai/types.ts +235 -0
- package/CHANGELOG.md +0 -614
- package/README.md +0 -467
- package/dist/browser/index.d.ts +0 -307
- package/dist/browser/index.js +0 -2
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-005 (TC-08) — conformance: the real `OpenAIProvider` over the shared MCP-005 fixture set.
|
|
3
|
+
*
|
|
4
|
+
* With `strictTools` off both the Responses and Chat Completions surfaces carry the permissive
|
|
5
|
+
* projection; with it on both carry the strict projection (§ Profiles — both surfaces honor
|
|
6
|
+
* `strictTools` identically). Adopted/adapted fixtures reach the request in projected form (compared
|
|
7
|
+
* against `projectToolSchema` directly, not hand-computed); rejected fixtures are absent; one
|
|
8
|
+
* `tool_schema_quarantined` warn per absent fixture names it. Both `chat` and `chatStream` request
|
|
9
|
+
* paths of each converter are exercised.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
PERMISSIVE_TOOL_SCHEMA_PROFILE,
|
|
13
|
+
projectToolSchema,
|
|
14
|
+
setGlobalLoggerSink,
|
|
15
|
+
STRICT_TOOL_SCHEMA_PROFILE,
|
|
16
|
+
} from '@robota-sdk/agent-core';
|
|
17
|
+
import { loadToolSchemaProjectionFixtures } from '@robota-sdk/agent-core/testing';
|
|
18
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
|
|
20
|
+
import { OpenAIProvider } from '../provider';
|
|
21
|
+
|
|
22
|
+
import type { IToolSchema, ILogger, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
23
|
+
|
|
24
|
+
vi.mock('openai', () => {
|
|
25
|
+
const MockOpenAI = vi.fn().mockImplementation(() => ({
|
|
26
|
+
chat: { completions: { create: vi.fn() } },
|
|
27
|
+
responses: { create: vi.fn() },
|
|
28
|
+
}));
|
|
29
|
+
return { default: MockOpenAI };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const fixtures = loadToolSchemaProjectionFixtures();
|
|
33
|
+
const PERMISSIVE_PROFILE = { ...PERMISSIVE_TOOL_SCHEMA_PROFILE, providerName: 'openai' };
|
|
34
|
+
const STRICT_PROFILE = { ...STRICT_TOOL_SCHEMA_PROFILE, providerName: 'openai' };
|
|
35
|
+
|
|
36
|
+
function recordingSink(): { sink: ILogger; lines: string[] } {
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
const record =
|
|
39
|
+
(level: string) =>
|
|
40
|
+
(...args: unknown[]): void => {
|
|
41
|
+
lines.push(`${level} ${args.map((value) => String(value)).join(' ')}`);
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
lines,
|
|
45
|
+
sink: {
|
|
46
|
+
debug: record('debug'),
|
|
47
|
+
info: record('info'),
|
|
48
|
+
warn: record('warn'),
|
|
49
|
+
error: record('error'),
|
|
50
|
+
log: record('log'),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createUserMessage(content: string): TUniversalMessage {
|
|
56
|
+
return { id: 'msg-1', state: 'complete' as const, role: 'user', content, timestamp: new Date() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface IFakeClient {
|
|
60
|
+
responses: { create: ReturnType<typeof vi.fn> };
|
|
61
|
+
chat: { completions: { create: ReturnType<typeof vi.fn> } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fakeClient(provider: OpenAIProvider): IFakeClient {
|
|
65
|
+
return (provider as unknown as { client: IFakeClient }).client;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function respondResponses(client: IFakeClient): void {
|
|
69
|
+
client.responses.create.mockResolvedValue({
|
|
70
|
+
id: 'resp-tool-schema-projection',
|
|
71
|
+
model: 'gpt-4o',
|
|
72
|
+
output_text: '',
|
|
73
|
+
output: [],
|
|
74
|
+
status: 'completed',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function respondChatCompletions(client: IFakeClient): void {
|
|
79
|
+
client.chat.completions.create.mockResolvedValue({
|
|
80
|
+
id: 'chatcmpl-tool-schema-projection',
|
|
81
|
+
object: 'chat.completion',
|
|
82
|
+
created: Date.now(),
|
|
83
|
+
model: 'gpt-4',
|
|
84
|
+
choices: [
|
|
85
|
+
{
|
|
86
|
+
index: 0,
|
|
87
|
+
message: { role: 'assistant', content: '', refusal: null },
|
|
88
|
+
finish_reason: 'stop',
|
|
89
|
+
logprobs: null,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function* streamResponsesResult() {
|
|
96
|
+
yield {
|
|
97
|
+
type: 'response.completed',
|
|
98
|
+
response: { id: 'resp-stream', output_text: '', output: [], status: 'completed' },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function* streamChatCompletionsResult() {
|
|
103
|
+
yield {
|
|
104
|
+
id: 'chunk-1',
|
|
105
|
+
object: 'chat.completion.chunk',
|
|
106
|
+
created: Date.now(),
|
|
107
|
+
model: 'gpt-4',
|
|
108
|
+
choices: [{ index: 0, delta: { content: '' }, finish_reason: 'stop', logprobs: null }],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ISentTool {
|
|
113
|
+
name: string;
|
|
114
|
+
description: string;
|
|
115
|
+
parameters: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sentToolsFromResponses(client: IFakeClient): ISentTool[] {
|
|
119
|
+
const [requestParams] = client.responses.create.mock.calls[
|
|
120
|
+
client.responses.create.mock.calls.length - 1
|
|
121
|
+
] as [{ tools?: Array<{ name: string; description: string; parameters: unknown }> }];
|
|
122
|
+
return (requestParams.tools ?? []).map((tool) => ({
|
|
123
|
+
name: tool.name,
|
|
124
|
+
description: tool.description,
|
|
125
|
+
parameters: tool.parameters,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sentToolsFromChatCompletions(client: IFakeClient): ISentTool[] {
|
|
130
|
+
const [requestParams] = client.chat.completions.create.mock.calls[
|
|
131
|
+
client.chat.completions.create.mock.calls.length - 1
|
|
132
|
+
] as [
|
|
133
|
+
{
|
|
134
|
+
tools?: Array<{ function: { name: string; description: string; parameters: unknown } }>;
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
return (requestParams.tools ?? []).map((tool) => ({
|
|
138
|
+
name: tool.function.name,
|
|
139
|
+
description: tool.function.description,
|
|
140
|
+
parameters: tool.function.parameters,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe.each([
|
|
145
|
+
{
|
|
146
|
+
apiSurface: 'responses' as const,
|
|
147
|
+
respond: respondResponses,
|
|
148
|
+
sentTools: sentToolsFromResponses,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
apiSurface: 'chat-completions' as const,
|
|
152
|
+
respond: respondChatCompletions,
|
|
153
|
+
sentTools: sentToolsFromChatCompletions,
|
|
154
|
+
},
|
|
155
|
+
])(
|
|
156
|
+
'MCP-005 TC-08 — OpenAI $apiSurface, permissive profile (strictTools off)',
|
|
157
|
+
({ apiSurface, respond, sentTools }) => {
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
setGlobalLoggerSink(undefined);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('reaches the request in projected form for adopted/adapted fixtures', async () => {
|
|
163
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface });
|
|
164
|
+
const client = fakeClient(provider);
|
|
165
|
+
respond(client);
|
|
166
|
+
|
|
167
|
+
const tools: IToolSchema[] = [
|
|
168
|
+
fixtures.subsetOnly,
|
|
169
|
+
fixtures.unknownKeywords,
|
|
170
|
+
fixtures.arrayItemsObject,
|
|
171
|
+
];
|
|
172
|
+
await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools });
|
|
173
|
+
|
|
174
|
+
const sent = sentTools(client);
|
|
175
|
+
expect(sent).toHaveLength(3);
|
|
176
|
+
for (const [index, tool] of tools.entries()) {
|
|
177
|
+
const projection = projectToolSchema(tool, PERMISSIVE_PROFILE);
|
|
178
|
+
expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
|
|
179
|
+
expect(sent[index]?.description).toEqual(projection.tool.description);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('omits a rejected fixture and reports one quarantine warn (chat)', async () => {
|
|
184
|
+
const recorder = recordingSink();
|
|
185
|
+
setGlobalLoggerSink(recorder.sink);
|
|
186
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface });
|
|
187
|
+
const client = fakeClient(provider);
|
|
188
|
+
respond(client);
|
|
189
|
+
|
|
190
|
+
const tools = [...fixtures.oneInvalidAmongMany] as IToolSchema[];
|
|
191
|
+
await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools });
|
|
192
|
+
|
|
193
|
+
const sent = sentTools(client);
|
|
194
|
+
expect(sent.map((tool) => tool.name)).toEqual(['good_tool_one', 'good_tool_two']);
|
|
195
|
+
|
|
196
|
+
const quarantineLines = recorder.lines.filter((line) =>
|
|
197
|
+
line.includes('tool_schema_quarantined'),
|
|
198
|
+
);
|
|
199
|
+
expect(quarantineLines).toHaveLength(1);
|
|
200
|
+
expect(quarantineLines[0]).toContain('provider=openai');
|
|
201
|
+
expect(quarantineLines[0]).toContain('tool=bad_tool');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('every documented rejection kind is absent from the request', async () => {
|
|
205
|
+
const recorder = recordingSink();
|
|
206
|
+
setGlobalLoggerSink(recorder.sink);
|
|
207
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface });
|
|
208
|
+
const client = fakeClient(provider);
|
|
209
|
+
respond(client);
|
|
210
|
+
|
|
211
|
+
const rejected: IToolSchema[] = [
|
|
212
|
+
fixtures.rootAnyOf,
|
|
213
|
+
fixtures.prototypeKey,
|
|
214
|
+
fixtures.deepNesting,
|
|
215
|
+
fixtures.oversized,
|
|
216
|
+
];
|
|
217
|
+
await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools: rejected });
|
|
218
|
+
|
|
219
|
+
expect(sentTools(client)).toHaveLength(0);
|
|
220
|
+
const quarantineLines = recorder.lines.filter((line) =>
|
|
221
|
+
line.includes('tool_schema_quarantined'),
|
|
222
|
+
);
|
|
223
|
+
expect(quarantineLines).toHaveLength(rejected.length);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('projects tools on the chatStream request path too', async () => {
|
|
227
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface });
|
|
228
|
+
const client = fakeClient(provider);
|
|
229
|
+
if (apiSurface === 'responses') {
|
|
230
|
+
client.responses.create.mockResolvedValue(streamResponsesResult());
|
|
231
|
+
} else {
|
|
232
|
+
client.chat.completions.create.mockResolvedValue(streamChatCompletionsResult());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const tools = [...fixtures.oneInvalidAmongMany] as IToolSchema[];
|
|
236
|
+
for await (const _chunk of provider.chatStream([createUserMessage('hello')], {
|
|
237
|
+
model: 'gpt-4o',
|
|
238
|
+
tools,
|
|
239
|
+
})) {
|
|
240
|
+
// drain
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sent = sentTools(client);
|
|
244
|
+
expect(sent.map((tool) => tool.name)).toEqual(['good_tool_one', 'good_tool_two']);
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
describe.each([
|
|
250
|
+
{
|
|
251
|
+
apiSurface: 'responses' as const,
|
|
252
|
+
respond: respondResponses,
|
|
253
|
+
sentTools: sentToolsFromResponses,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
apiSurface: 'chat-completions' as const,
|
|
257
|
+
respond: respondChatCompletions,
|
|
258
|
+
sentTools: sentToolsFromChatCompletions,
|
|
259
|
+
},
|
|
260
|
+
])(
|
|
261
|
+
'MCP-005 TC-08 — OpenAI $apiSurface, strict profile (strictTools on)',
|
|
262
|
+
({ apiSurface, respond, sentTools }) => {
|
|
263
|
+
it('reaches the request in strictly-projected form', async () => {
|
|
264
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface, strictTools: true });
|
|
265
|
+
const client = fakeClient(provider);
|
|
266
|
+
respond(client);
|
|
267
|
+
|
|
268
|
+
const tools: IToolSchema[] = [fixtures.subsetOnly, fixtures.nestedAnyOfOptional];
|
|
269
|
+
await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools });
|
|
270
|
+
|
|
271
|
+
const sent = sentTools(client);
|
|
272
|
+
expect(sent).toHaveLength(2);
|
|
273
|
+
for (const [index, tool] of tools.entries()) {
|
|
274
|
+
const projection = projectToolSchema(tool, STRICT_PROFILE);
|
|
275
|
+
expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
|
|
276
|
+
expect(sent[index]?.description).toEqual(projection.tool.description);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('projects strictly on the chatStream request path too', async () => {
|
|
281
|
+
const provider = new OpenAIProvider({ apiKey: 'sk-test', apiSurface, strictTools: true });
|
|
282
|
+
const client = fakeClient(provider);
|
|
283
|
+
if (apiSurface === 'responses') {
|
|
284
|
+
client.responses.create.mockResolvedValue(streamResponsesResult());
|
|
285
|
+
} else {
|
|
286
|
+
client.chat.completions.create.mockResolvedValue(streamChatCompletionsResult());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const tools: IToolSchema[] = [fixtures.subsetOnly];
|
|
290
|
+
for await (const _chunk of provider.chatStream([createUserMessage('hello')], {
|
|
291
|
+
model: 'gpt-4o',
|
|
292
|
+
tools,
|
|
293
|
+
})) {
|
|
294
|
+
// drain
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const sent = sentTools(client);
|
|
298
|
+
const projection = projectToolSchema(fixtures.subsetOnly, STRICT_PROFILE);
|
|
299
|
+
expect(sent[0]?.parameters).toEqual(projection.tool.parameters);
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted `traceparent` on the wire: captured at the SDK's own fetch, for every call site on both
|
|
3
|
+
* API surfaces.
|
|
4
|
+
*/
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { OpenAIProvider } from '../provider';
|
|
9
|
+
|
|
10
|
+
import type { IChatOptions, IOutboundTraceContext, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
11
|
+
|
|
12
|
+
const TRACEPARENT = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
|
|
13
|
+
|
|
14
|
+
const completion = {
|
|
15
|
+
id: 'c1', object: 'chat.completion', created: 0, model: 'gpt-test',
|
|
16
|
+
choices: [{ index: 0, message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }],
|
|
17
|
+
};
|
|
18
|
+
const completionChunks = [
|
|
19
|
+
{ id: 'c1', object: 'chat.completion.chunk', created: 0, model: 'gpt-test', choices: [{ index: 0, delta: { role: 'assistant', content: 'hi' }, finish_reason: null }] },
|
|
20
|
+
{ id: 'c1', object: 'chat.completion.chunk', created: 0, model: 'gpt-test', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] },
|
|
21
|
+
];
|
|
22
|
+
const response = {
|
|
23
|
+
id: 'resp_1', object: 'response', created_at: 0, model: 'gpt-test', status: 'completed',
|
|
24
|
+
output: [{ type: 'message', id: 'msg_1', role: 'assistant', status: 'completed', content: [{ type: 'output_text', text: 'hi', annotations: [] }] }],
|
|
25
|
+
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
|
26
|
+
};
|
|
27
|
+
const responseEvents = [
|
|
28
|
+
{ type: 'response.output_text.delta', item_id: 'msg_1', output_index: 0, content_index: 0, delta: 'hi', sequence_number: 1 },
|
|
29
|
+
{ type: 'response.completed', response, sequence_number: 2 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function sse(events: readonly object[], done: boolean): string {
|
|
33
|
+
return events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join('') + (done ? 'data: [DONE]\n\n' : '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function wire(baseURL?: string) {
|
|
37
|
+
const seen: Array<{ url: string; traceparent: string | null }> = [];
|
|
38
|
+
const fetch = async (url: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
39
|
+
const target = String(url);
|
|
40
|
+
seen.push({ url: target, traceparent: new Headers(init?.headers as HeadersInit).get('traceparent') });
|
|
41
|
+
const body = JSON.parse(String(init?.body)) as { stream?: boolean };
|
|
42
|
+
const responses = target.endsWith('/responses');
|
|
43
|
+
if (body.stream) {
|
|
44
|
+
return new Response(sse(responses ? responseEvents : completionChunks, !responses), {
|
|
45
|
+
status: 200, headers: { 'content-type': 'text/event-stream', 'x-request-id': 'req_1' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return new Response(JSON.stringify(responses ? response : completion), {
|
|
49
|
+
status: 200, headers: { 'content-type': 'application/json', 'x-request-id': 'req_1' },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const client = new OpenAI({ apiKey: 'test-key', ...(baseURL ? { baseURL } : {}), fetch: fetch as never, maxRetries: 0 });
|
|
53
|
+
return { client, seen };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const messages: TUniversalMessage[] = [{ id: 'u1', role: 'user', content: 'hello', state: 'complete', timestamp: new Date() }];
|
|
57
|
+
|
|
58
|
+
function traceTo(...allowedOrigins: string[]): IOutboundTraceContext {
|
|
59
|
+
return { traceparent: TRACEPARENT, allowedOrigins };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** chat() without and with a delta callback, then chatStream(): every call site of one surface. */
|
|
63
|
+
async function everyCallSite(provider: OpenAIProvider, options: IChatOptions): Promise<void> {
|
|
64
|
+
await provider.chat(messages, { model: 'gpt-test', ...options });
|
|
65
|
+
await provider.chat(messages, { model: 'gpt-test', onTextDelta: () => undefined, ...options });
|
|
66
|
+
for await (const _chunk of provider.chatStream(messages, { model: 'gpt-test', ...options })) {
|
|
67
|
+
// drain
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('OpenAI trusted trace context', () => {
|
|
72
|
+
for (const apiSurface of ['chat-completions', 'responses'] as const) {
|
|
73
|
+
it(`sends traceparent from every ${apiSurface} call site when the client origin is listed`, async () => {
|
|
74
|
+
const { client, seen } = wire('https://gateway.example.com/v1');
|
|
75
|
+
const provider = new OpenAIProvider({ client, apiSurface });
|
|
76
|
+
const raw: unknown[] = [];
|
|
77
|
+
await everyCallSite(provider, {
|
|
78
|
+
outboundTraceContext: traceTo('https://gateway.example.com'),
|
|
79
|
+
onProviderNativeRawPayload: (event) => raw.push(event),
|
|
80
|
+
});
|
|
81
|
+
expect(provider.canPropagateTraceContext()).toBe(true);
|
|
82
|
+
expect(seen).toHaveLength(3);
|
|
83
|
+
expect(seen.map((request) => request.traceparent)).toEqual([TRACEPARENT, TRACEPARENT, TRACEPARENT]);
|
|
84
|
+
expect(raw.length).toBeGreaterThan(0);
|
|
85
|
+
expect(JSON.stringify(raw)).not.toContain('traceparent');
|
|
86
|
+
expect(JSON.stringify(raw)).not.toContain(TRACEPARENT);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it(`sends nothing from ${apiSurface} to the default vendor origin unless it is listed`, async () => {
|
|
90
|
+
const unlisted = wire();
|
|
91
|
+
await everyCallSite(new OpenAIProvider({ client: unlisted.client, apiSurface }), {
|
|
92
|
+
outboundTraceContext: traceTo('https://gateway.example.com'),
|
|
93
|
+
});
|
|
94
|
+
expect(unlisted.seen.map((request) => request.traceparent)).toEqual([null, null, null]);
|
|
95
|
+
|
|
96
|
+
const listed = wire();
|
|
97
|
+
await everyCallSite(new OpenAIProvider({ client: listed.client, apiSurface }), {
|
|
98
|
+
outboundTraceContext: traceTo('https://api.openai.com'),
|
|
99
|
+
});
|
|
100
|
+
expect(listed.seen.map((request) => request.traceparent)).toEqual([TRACEPARENT, TRACEPARENT, TRACEPARENT]);
|
|
101
|
+
|
|
102
|
+
const none = wire();
|
|
103
|
+
await everyCallSite(new OpenAIProvider({ client: none.client, apiSurface }), {});
|
|
104
|
+
expect(none.seen.map((request) => request.traceparent)).toEqual([null, null, null]);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
it('cannot propagate through an executor or a client whose base URL is unreadable', () => {
|
|
109
|
+
const executor = { name: 'remote', version: '1', executeChat: async () => ({}), supportsTools: () => true, validateConfig: () => true };
|
|
110
|
+
expect(new OpenAIProvider({ executor } as never).canPropagateTraceContext()).toBe(false);
|
|
111
|
+
const opaque = { chat: { completions: { create: () => undefined } } } as unknown as OpenAI;
|
|
112
|
+
expect(new OpenAIProvider({ client: opaque }).canPropagateTraceContext()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|