feat: harden public OpenAI API contracts
This commit is contained in:
Generated
+39
@@ -33,6 +33,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"openai": "7.10.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
@@ -9451,6 +9452,44 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-7.10.0.tgz",
|
||||
"integrity": "sha512-sn9t2Kls7O52PwuF9BUTYNu4Gk/r0lXJyrgaNht4TNRlZFb3dJIGO0RciSgjARGCBRtWjySubAQFJttlzUvGQQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
|
||||
"@smithy/hash-node": ">=4.3.0 <5",
|
||||
"@smithy/signature-v4": ">=5.4.0 <6",
|
||||
"undici": ">=5 <9",
|
||||
"ws": "^8.21.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-provider-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/hash-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/signature-v4": {
|
||||
"optional": true
|
||||
},
|
||||
"undici": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"test:contracts": "node --test tests/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.90.0",
|
||||
@@ -34,6 +35,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"openai": "7.10.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import OpenAI from "openai";
|
||||
|
||||
|
||||
function jsonResponse(value, status = 200, extraHeaders = {}) {
|
||||
return new Response(JSON.stringify(value), {
|
||||
status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Request-ID": "req_js_contract",
|
||||
...extraHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function inputItems(input) {
|
||||
if (typeof input === "string") {
|
||||
return [
|
||||
{
|
||||
id: "msg_input_contract",
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: input }],
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
}
|
||||
return input.map((item, index) => ({
|
||||
id: item.id ?? `item_contract_${index}`,
|
||||
status: item.status ?? "completed",
|
||||
...item,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function completedResponse({ id, model, previousResponseId, input, output }) {
|
||||
return {
|
||||
id,
|
||||
object: "response",
|
||||
created_at: 1788739200,
|
||||
completed_at: 1788739201,
|
||||
status: "completed",
|
||||
error: null,
|
||||
incomplete_details: null,
|
||||
instructions: null,
|
||||
model,
|
||||
output,
|
||||
parallel_tool_calls: false,
|
||||
previous_response_id: previousResponseId,
|
||||
store: true,
|
||||
tool_choice: "auto",
|
||||
tools: [],
|
||||
metadata: {},
|
||||
usage: null,
|
||||
_input: inputItems(input),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function publicResponse(response) {
|
||||
const value = { ...response };
|
||||
delete value._input;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
function sseResponse(response) {
|
||||
const initial = {
|
||||
...publicResponse(response),
|
||||
status: "in_progress",
|
||||
completed_at: null,
|
||||
output: [],
|
||||
};
|
||||
const item = response.output[0];
|
||||
const events = [];
|
||||
const append = (event) => {
|
||||
events.push({ ...event, sequence_number: events.length });
|
||||
};
|
||||
append({ type: "response.created", response: initial });
|
||||
append({ type: "response.in_progress", response: initial });
|
||||
if (item.type === "message") {
|
||||
const part = item.content[0];
|
||||
append({
|
||||
type: "response.output_item.added",
|
||||
output_index: 0,
|
||||
item: { ...item, status: "in_progress", content: [] },
|
||||
});
|
||||
append({
|
||||
type: "response.content_part.added",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
content_index: 0,
|
||||
part: { type: "output_text", text: "", annotations: [] },
|
||||
});
|
||||
append({
|
||||
type: "response.output_text.delta",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
content_index: 0,
|
||||
delta: part.text,
|
||||
logprobs: [],
|
||||
});
|
||||
append({
|
||||
type: "response.output_text.done",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
content_index: 0,
|
||||
text: part.text,
|
||||
logprobs: [],
|
||||
});
|
||||
append({
|
||||
type: "response.content_part.done",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
content_index: 0,
|
||||
part,
|
||||
});
|
||||
} else {
|
||||
append({
|
||||
type: "response.output_item.added",
|
||||
output_index: 0,
|
||||
item: { ...item, status: "in_progress", arguments: "" },
|
||||
});
|
||||
append({
|
||||
type: "response.function_call_arguments.delta",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
delta: item.arguments,
|
||||
});
|
||||
append({
|
||||
type: "response.function_call_arguments.done",
|
||||
item_id: item.id,
|
||||
output_index: 0,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
});
|
||||
}
|
||||
append({ type: "response.output_item.done", output_index: 0, item });
|
||||
append({ type: "response.completed", response: publicResponse(response) });
|
||||
const body = events
|
||||
.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`)
|
||||
.join("");
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"X-Request-ID": "req_js_contract",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function createContractFetch() {
|
||||
const responses = new Map();
|
||||
const idempotency = new Map();
|
||||
let sequence = 0;
|
||||
|
||||
const fetch = async (input, init) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init);
|
||||
assert.match(request.headers.get("authorization") ?? "", /^Bearer /);
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/v1/models") {
|
||||
return jsonResponse({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "assistant:asst_js",
|
||||
object: "model",
|
||||
created: 1788739200,
|
||||
owned_by: "interactive-media",
|
||||
name: "JS contract assistant",
|
||||
capabilities: ["responses", "realtime"],
|
||||
assistant_id: "asst_js",
|
||||
assistant_type: "prompt",
|
||||
runtime_mode: "pipeline",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/v1/responses") {
|
||||
const body = await request.clone().json();
|
||||
const idempotencyKey = request.headers.get("idempotency-key");
|
||||
const fingerprint = JSON.stringify(body);
|
||||
if (idempotencyKey && idempotency.has(idempotencyKey)) {
|
||||
const stored = idempotency.get(idempotencyKey);
|
||||
if (stored.fingerprint !== fingerprint) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: {
|
||||
type: "invalid_request_error",
|
||||
code: "idempotency_key_conflict",
|
||||
message: "Idempotency-Key was used with a different request",
|
||||
param: "Idempotency-Key",
|
||||
},
|
||||
},
|
||||
409,
|
||||
);
|
||||
}
|
||||
return jsonResponse(publicResponse(stored.response), 200, {
|
||||
"X-Idempotent-Replayed": "true",
|
||||
});
|
||||
}
|
||||
|
||||
const parent = body.previous_response_id
|
||||
? responses.get(body.previous_response_id)
|
||||
: undefined;
|
||||
const model = body.model ?? parent?.model;
|
||||
sequence += 1;
|
||||
const responseId = `resp_js_${sequence}`;
|
||||
const isToolCall = body.input === "调用客户端工具";
|
||||
const output = isToolCall
|
||||
? [
|
||||
{
|
||||
id: "fc_js_contract",
|
||||
type: "function_call",
|
||||
status: "completed",
|
||||
call_id: "call_js_contract",
|
||||
name: "show_message",
|
||||
arguments: '{"message":"请确认"}',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: `msg_js_${sequence}`,
|
||||
type: "message",
|
||||
status: "completed",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "output_text",
|
||||
text: `第 ${sequence} 轮回复`,
|
||||
annotations: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const response = completedResponse({
|
||||
id: responseId,
|
||||
model,
|
||||
previousResponseId: body.previous_response_id ?? null,
|
||||
input: body.input,
|
||||
output,
|
||||
});
|
||||
responses.set(responseId, response);
|
||||
if (idempotencyKey) {
|
||||
idempotency.set(idempotencyKey, { fingerprint, response });
|
||||
}
|
||||
return body.stream ? sseResponse(response) : jsonResponse(publicResponse(response));
|
||||
}
|
||||
|
||||
const inputItemsMatch = url.pathname.match(
|
||||
/^\/v1\/responses\/([^/]+)\/input_items$/,
|
||||
);
|
||||
if (request.method === "GET" && inputItemsMatch) {
|
||||
const response = responses.get(inputItemsMatch[1]);
|
||||
if (!response) return jsonResponse({ error: { message: "Not found" } }, 404);
|
||||
const data = response._input;
|
||||
return jsonResponse({
|
||||
object: "list",
|
||||
data,
|
||||
first_id: data[0]?.id ?? null,
|
||||
last_id: data.at(-1)?.id ?? null,
|
||||
has_more: false,
|
||||
});
|
||||
}
|
||||
|
||||
const responseMatch = url.pathname.match(/^\/v1\/responses\/([^/]+)$/);
|
||||
if (responseMatch && request.method === "GET") {
|
||||
const response = responses.get(responseMatch[1]);
|
||||
return response
|
||||
? jsonResponse(publicResponse(response))
|
||||
: jsonResponse(
|
||||
{
|
||||
error: {
|
||||
type: "invalid_request_error",
|
||||
code: "response_not_found",
|
||||
message: "Response not found",
|
||||
param: null,
|
||||
},
|
||||
},
|
||||
404,
|
||||
);
|
||||
}
|
||||
if (responseMatch && request.method === "DELETE") {
|
||||
const deleted = responses.delete(responseMatch[1]);
|
||||
return deleted
|
||||
? jsonResponse({ id: responseMatch[1], object: "response", deleted: true })
|
||||
: jsonResponse({ error: { message: "Not found" } }, 404);
|
||||
}
|
||||
return jsonResponse({ error: { message: "Unexpected contract request" } }, 500);
|
||||
};
|
||||
|
||||
return { fetch, responses };
|
||||
}
|
||||
|
||||
|
||||
test("OpenAPI snapshot exposes the public Responses surface", () => {
|
||||
const openapiPath = new URL("../../backend/contracts/openapi.json", import.meta.url);
|
||||
const openapi = JSON.parse(readFileSync(openapiPath, "utf8"));
|
||||
assert.ok(openapi.paths["/v1/responses"].post);
|
||||
assert.ok(openapi.paths["/v1/responses/{response_id}/input_items"].get);
|
||||
assert.ok(openapi.paths["/v1/responses/{response_id}"].delete);
|
||||
assert.ok(openapi.paths["/v1/models"].get);
|
||||
});
|
||||
|
||||
|
||||
test("official JavaScript SDK completes three stateful turns and discovery", async () => {
|
||||
const contract = createContractFetch();
|
||||
const client = new OpenAI({
|
||||
apiKey: "sk-rt-js-contract",
|
||||
baseURL: "https://contract.test/v1",
|
||||
fetch: contract.fetch,
|
||||
maxRetries: 0,
|
||||
});
|
||||
|
||||
const models = await client.models.list();
|
||||
assert.equal(models.data[0].id, "assistant:asst_js");
|
||||
|
||||
const first = await client.responses.create({
|
||||
model: "assistant:asst_js",
|
||||
input: "第一轮",
|
||||
});
|
||||
const second = await client.responses.create({
|
||||
previous_response_id: first.id,
|
||||
input: "第二轮",
|
||||
});
|
||||
const third = await client.responses.create({
|
||||
previous_response_id: second.id,
|
||||
input: "第三轮",
|
||||
});
|
||||
assert.equal(third.previous_response_id, second.id);
|
||||
assert.equal(third.model, "assistant:asst_js");
|
||||
assert.equal(third.output_text, "第 3 轮回复");
|
||||
|
||||
const page = await client.responses.inputItems.list(third.id, { order: "asc" });
|
||||
assert.equal(page.data[0].type, "message");
|
||||
const retrieved = await client.responses.retrieve(third.id);
|
||||
assert.equal(retrieved.id, third.id);
|
||||
|
||||
const deleted = await client.responses.delete(first.id);
|
||||
assert.equal(deleted.deleted, true);
|
||||
});
|
||||
|
||||
|
||||
test("official JavaScript SDK parses SSE and Client Tool continuation", async () => {
|
||||
const contract = createContractFetch();
|
||||
const client = new OpenAI({
|
||||
apiKey: "sk-rt-js-contract",
|
||||
baseURL: "https://contract.test/v1",
|
||||
fetch: contract.fetch,
|
||||
maxRetries: 0,
|
||||
});
|
||||
|
||||
const stream = await client.responses.create({
|
||||
model: "assistant:asst_js",
|
||||
input: "流式输出",
|
||||
stream: true,
|
||||
});
|
||||
const events = [];
|
||||
for await (const event of stream) events.push(event);
|
||||
assert.deepEqual(
|
||||
events.map((event) => event.sequence_number),
|
||||
Array.from({ length: events.length }, (_, index) => index),
|
||||
);
|
||||
assert.equal(events.at(-1).type, "response.completed");
|
||||
const streamed = await client.responses.retrieve(
|
||||
events.at(-1).response.id,
|
||||
);
|
||||
assert.equal(
|
||||
streamed.output_text,
|
||||
events.at(-1).response.output[0].content[0].text,
|
||||
);
|
||||
|
||||
const toolStream = await client.responses.create({
|
||||
model: "assistant:asst_js",
|
||||
input: "调用客户端工具",
|
||||
stream: true,
|
||||
});
|
||||
const toolEvents = [];
|
||||
for await (const event of toolStream) toolEvents.push(event);
|
||||
assert.ok(
|
||||
toolEvents.some(
|
||||
(event) => event.type === "response.function_call_arguments.done",
|
||||
),
|
||||
);
|
||||
const tool = toolEvents.at(-1).response;
|
||||
const continued = await client.responses.create({
|
||||
previous_response_id: tool.id,
|
||||
input: [
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: tool.output[0].call_id,
|
||||
output: '{"status":"ok"}',
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(continued.status, "completed");
|
||||
});
|
||||
|
||||
|
||||
test("official JavaScript SDK preserves idempotency semantics", async () => {
|
||||
const contract = createContractFetch();
|
||||
const client = new OpenAI({
|
||||
apiKey: "sk-rt-js-contract",
|
||||
baseURL: "https://contract.test/v1",
|
||||
fetch: contract.fetch,
|
||||
maxRetries: 0,
|
||||
});
|
||||
const options = { headers: { "Idempotency-Key": "js-contract-key" } };
|
||||
const request = { model: "assistant:asst_js", input: "幂等请求" };
|
||||
|
||||
const first = await client.responses.create(request, options);
|
||||
const replay = await client.responses.create(request, options);
|
||||
assert.equal(replay.id, first.id);
|
||||
await assert.rejects(
|
||||
client.responses.create(
|
||||
{ model: "assistant:asst_js", input: "不同请求" },
|
||||
options,
|
||||
),
|
||||
(error) => error.status === 409,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user