refactor: align provider and task contracts
This commit is contained in:
@@ -11,8 +11,12 @@ function channel(store: ReturnType<typeof createStore>) {
|
||||
...store.channels[0],
|
||||
providerType: "openai-images" as const,
|
||||
baseUrl: "https://1.1.1.1/v1",
|
||||
providerModelId: "base-model",
|
||||
resolutionModelMap: { "2K": "mapped-2k" },
|
||||
models: [
|
||||
{ requestModelId: "base-model", source: "manual" as const, enabled: true },
|
||||
{ requestModelId: "mapped-2k", source: "manual" as const, enabled: true },
|
||||
{ requestModelId: "供应商模型", source: "manual" as const, enabled: true },
|
||||
],
|
||||
modelMappings: [],
|
||||
secretRef: encryptSecret("1234567890123456", store.channelEncryptionKey),
|
||||
};
|
||||
}
|
||||
@@ -26,14 +30,14 @@ test("OpenAI image generation uses the mapped model and normalized v1 endpoint",
|
||||
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json", "x-request-id": "request-123" } });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-1", taskType: "image", prompt: "一只猫", count: 1, params: { resolution: "2K" }, platformIdempotencyKey: "idem-1" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-1", taskType: "image", requestModelId: "base-model", prompt: "一只猫", count: 1, params: { resolution: "2K" }, platformIdempotencyKey: "idem-1" });
|
||||
assert.equal(result.status, "succeeded");
|
||||
assert.equal(result.providerRequestId, "request-123");
|
||||
assert.equal(result.outputs?.[0]?.mimeType, "image/png");
|
||||
assert.equal(requests[0]?.url, "https://1.1.1.1/v1/images/generations");
|
||||
assert.equal(requests[0]?.headers.get("authorization"), "Bearer 1234567890123456");
|
||||
assert.equal(requests[0]?.headers.get("idempotency-key"), "idem-1");
|
||||
assert.equal((JSON.parse(String(requests[0]?.body)) as Record<string, unknown>).model, "mapped-2k");
|
||||
assert.equal((JSON.parse(String(requests[0]?.body)) as Record<string, unknown>).model, "base-model");
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
}
|
||||
@@ -72,7 +76,7 @@ test("OpenAI image edits preserve reference order with image[] fields", async ()
|
||||
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-2", taskType: "image", prompt: "@图片一和@图片二", count: 1, referenceImages: [{ objectId: "ref-b", seq: 2, name: "图片二" }, { objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-2" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-2", taskType: "image", requestModelId: "base-model", prompt: "@图片一和@图片二", count: 1, referenceImages: [{ objectId: "ref-b", seq: 2, name: "图片二" }, { objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-2" });
|
||||
assert.equal(result.status, "succeeded");
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
@@ -87,7 +91,7 @@ test("invalid image base64 and empty model probes fail closed", async () => {
|
||||
return new Response(JSON.stringify({ data: [{ b64_json: "not-base64" }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-3", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "idem-3" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-3", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "idem-3" });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
|
||||
await assert.rejects(() => probeProviderModels("https://1.1.1.1/v1", "1234567890123456"));
|
||||
@@ -113,7 +117,7 @@ test("duplicate references are uploaded once and URL outputs are validated", asy
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), {
|
||||
taskId: "task-4",
|
||||
taskId: "task-4", requestModelId: "base-model",
|
||||
taskType: "image",
|
||||
prompt: "使用@图片一",
|
||||
count: 1,
|
||||
@@ -141,7 +145,7 @@ test("unavailable reference objects fail without retrying the provider", async (
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), {
|
||||
taskId: "task-5",
|
||||
taskId: "task-5", requestModelId: "base-model",
|
||||
taskType: "image",
|
||||
prompt: "使用第一张图",
|
||||
count: 1,
|
||||
@@ -166,7 +170,7 @@ test("expired, revoked, and missing staging references fail deterministically",
|
||||
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
|
||||
try {
|
||||
for (const [index, objectId] of ["expired", "revoked", "staging-missing"].entries()) {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `reference-unavailable-${index}`, taskType: "image", prompt: "使用第一张图", count: 1, referenceImages: [{ objectId, seq: 1, name: "图片一" }], platformIdempotencyKey: `reference-unavailable-${index}` });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `reference-unavailable-${index}`, taskType: "image", requestModelId: "base-model", prompt: "使用第一张图", count: 1, referenceImages: [{ objectId, seq: 1, name: "图片一" }], platformIdempotencyKey: `reference-unavailable-${index}` });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
|
||||
assert.equal(result.retryable, false);
|
||||
@@ -191,7 +195,7 @@ test("invalid image output URLs are non-retryable provider failures", async () =
|
||||
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: outputUrl }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
return new Response(index === 2 ? png : "not an image", { status: index === 2 ? 404 : 200, headers: { "content-type": "image/png" } });
|
||||
};
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `invalid-output-${index}`, taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: `invalid-output-${index}` });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `invalid-output-${index}`, taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: `invalid-output-${index}` });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
|
||||
assert.equal(result.retryable, false);
|
||||
@@ -211,7 +215,7 @@ test("redirected image output URLs are rejected as invalid output", async () =>
|
||||
return new Response(null, { status: 302, headers: { location: "https://example.com/private.png" } });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "redirect-output", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "redirect-output" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "redirect-output", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "redirect-output" });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
|
||||
assert.equal(result.retryable, false);
|
||||
@@ -229,7 +233,7 @@ test("image output metadata prefers dimensions decoded from the returned bytes",
|
||||
return new Response(null, { status: 500 });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "metadata-bytes", taskType: "image", prompt: "测试", count: 1, platformIdempotencyKey: "metadata-bytes" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "metadata-bytes", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "metadata-bytes" });
|
||||
assert.equal(result.status, "succeeded");
|
||||
assert.deepEqual(result.outputs?.[0]?.metadata, { width: 1, height: 1, format: "png", size: "1x1", revisedPrompt: undefined, usage: undefined, source: "base64" });
|
||||
} finally {
|
||||
@@ -248,10 +252,10 @@ test("image references validate mentions and add a default reference instruction
|
||||
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
try {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-6", taskType: "image", prompt: "请保持主体", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-6" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-6", taskType: "image", requestModelId: "base-model", prompt: "请保持主体", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-6" });
|
||||
assert.equal(result.status, "succeeded");
|
||||
assert.equal(prompts[0], "请保持主体\n基于以下参考图生成。");
|
||||
const missing = await invokeProvider(store, channel(store), { taskId: "task-7", taskType: "image", prompt: "使用@图片二", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-7" });
|
||||
const missing = await invokeProvider(store, channel(store), { taskId: "task-7", taskType: "image", requestModelId: "base-model", prompt: "使用@图片二", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-7" });
|
||||
assert.equal(missing.status, "failed");
|
||||
assert.equal(missing.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
|
||||
} finally {
|
||||
@@ -261,7 +265,7 @@ test("image references validate mentions and add a default reference instruction
|
||||
|
||||
test("invalid provider reference sequences fail closed", async () => {
|
||||
const store = createStore();
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-8", taskType: "image", prompt: "测试", count: 1, referenceImages: [{ objectId: "ref-a", seq: 17, name: "图片十七" }], platformIdempotencyKey: "idem-8" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "task-8", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, referenceImages: [{ objectId: "ref-a", seq: 17, name: "图片十七" }], platformIdempotencyKey: "idem-8" });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
|
||||
});
|
||||
@@ -269,7 +273,7 @@ test("invalid provider reference sequences fail closed", async () => {
|
||||
test("duplicate reference sequences are rejected instead of producing ambiguous 图片 names", async () => {
|
||||
const store = createStore();
|
||||
for (const objectId of ["duplicate-seq-a", "duplicate-seq-b"]) store.objects.set(objectId, { id: objectId, ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "duplicate-seq", taskType: "image", prompt: "测试", count: 1, referenceImages: [{ objectId: "duplicate-seq-a", seq: 1, name: "图片一" }, { objectId: "duplicate-seq-b", seq: 1, name: "图片一" }], platformIdempotencyKey: "duplicate-seq" });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: "duplicate-seq", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, referenceImages: [{ objectId: "duplicate-seq-a", seq: 1, name: "图片一" }, { objectId: "duplicate-seq-b", seq: 1, name: "图片一" }], platformIdempotencyKey: "duplicate-seq" });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
|
||||
});
|
||||
@@ -281,7 +285,7 @@ test("provider image requests reject non-positive or fractional counts", async (
|
||||
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
|
||||
try {
|
||||
for (const count of [0, -1, 1.5]) {
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `invalid-count-${count}`, taskType: "image", prompt: "测试", count, platformIdempotencyKey: `invalid-count-${String(count).replace(".", "-")}` });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `invalid-count-${count}`, taskType: "image", requestModelId: "base-model", prompt: "测试", count, platformIdempotencyKey: `invalid-count-${String(count).replace(".", "-")}` });
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.errorCode, "PROVIDER_INVALID_REQUEST");
|
||||
}
|
||||
@@ -305,9 +309,9 @@ test("OpenAI image edits accept the full sixteen-reference boundary and reject t
|
||||
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
try {
|
||||
const accepted = await invokeProvider(store, channel(store), { taskId: "sixteen-references", taskType: "image", prompt: "组合参考图", count: 1, referenceImages: references, platformIdempotencyKey: "sixteen-references" });
|
||||
const accepted = await invokeProvider(store, channel(store), { taskId: "sixteen-references", taskType: "image", requestModelId: "base-model", prompt: "组合参考图", count: 1, referenceImages: references, platformIdempotencyKey: "sixteen-references" });
|
||||
assert.equal(accepted.status, "succeeded");
|
||||
const rejected = await invokeProvider(store, channel(store), { taskId: "seventeen-references", taskType: "image", prompt: "组合参考图", count: 1, referenceImages: [...references, { objectId: "boundary-ref-1", seq: 17, name: "图片十七" }], platformIdempotencyKey: "seventeen-references" });
|
||||
const rejected = await invokeProvider(store, channel(store), { taskId: "seventeen-references", taskType: "image", requestModelId: "base-model", prompt: "组合参考图", count: 1, referenceImages: [...references, { objectId: "boundary-ref-1", seq: 17, name: "图片十七" }], platformIdempotencyKey: "seventeen-references" });
|
||||
assert.equal(rejected.status, "failed");
|
||||
assert.equal(rejected.errorCode, "PROVIDER_REFERENCE_LIMIT");
|
||||
} finally {
|
||||
@@ -331,7 +335,7 @@ test("provider output metadata detects JPEG and WebP bytes instead of trusting a
|
||||
try {
|
||||
for (const [index, suffix] of ["jpeg", "webp"].entries()) {
|
||||
requestedFormat = suffix;
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `format-${suffix}`, taskType: "image", prompt: "格式测试", count: 1, params: { output_format: suffix }, platformIdempotencyKey: `format-${index}` });
|
||||
const result = await invokeProvider(store, channel(store), { taskId: `format-${suffix}`, taskType: "image", requestModelId: "base-model", prompt: "格式测试", count: 1, params: { output_format: suffix }, platformIdempotencyKey: `format-${index}` });
|
||||
assert.equal(result.status, "succeeded");
|
||||
assert.equal(result.outputs?.[0]?.mimeType, `image/${suffix}`);
|
||||
assert.deepEqual(result.outputs?.[0]?.metadata && { width: result.outputs[0].metadata.width, height: result.outputs[0].metadata.height, format: result.outputs[0].metadata.format }, { width: 1, height: 1, format: suffix });
|
||||
@@ -356,12 +360,12 @@ test("provider probes use GET and Bearer authentication, and transport failures
|
||||
const probe = await probeProviderModels("https://1.1.1.1/v1", "1234567890123456");
|
||||
assert.deepEqual(probe, { healthy: true, models: ["gpt-image-2"] });
|
||||
assert.deepEqual(probeRequests, [{ method: "GET", authorization: "Bearer 1234567890123456" }]);
|
||||
const unknown = await invokeProvider(store, channel(store), { taskId: "transport-unknown", taskType: "image", prompt: "网络中断", count: 1, platformIdempotencyKey: "transport-unknown" });
|
||||
const unknown = await invokeProvider(store, channel(store), { taskId: "transport-unknown", taskType: "image", requestModelId: "base-model", prompt: "网络中断", count: 1, platformIdempotencyKey: "transport-unknown" });
|
||||
assert.equal(unknown.status, "unknown");
|
||||
assert.equal(unknown.errorCode, "PROVIDER_TRANSPORT_UNKNOWN");
|
||||
const abortedFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => { throw Object.assign(new Error("timeout"), { name: "AbortError" }); };
|
||||
const timeout = await invokeProvider(store, channel(store), { taskId: "timeout-unknown", taskType: "image", prompt: "超时", count: 1, platformIdempotencyKey: "timeout-unknown" });
|
||||
const timeout = await invokeProvider(store, channel(store), { taskId: "timeout-unknown", taskType: "image", requestModelId: "base-model", prompt: "超时", count: 1, platformIdempotencyKey: "timeout-unknown" });
|
||||
assert.equal(timeout.status, "unknown");
|
||||
assert.equal(timeout.errorCode, "PROVIDER_TIMEOUT_UNKNOWN");
|
||||
globalThis.fetch = async () => { throw new Error("probe transport failed"); };
|
||||
|
||||
Reference in New Issue
Block a user