fix(proxy): use credentials.connectionId instead of non-existent credentials.id for image proxy resolution (#1929)

Integrated into release/v3.7.9
This commit is contained in:
Aculeasis
2026-05-05 01:23:27 +03:00
committed by GitHub
parent 8f3d9e2ec7
commit 3f900a833e
3 changed files with 119 additions and 8 deletions

View File

@@ -229,9 +229,9 @@ export async function POST(request) {
// Resolve proxy for the connection if credentials exist (#1904)
let proxyInfo = null;
if (credentials?.id) {
if (credentials?.connectionId) {
try {
proxyInfo = await resolveProxyForConnection(credentials.id);
proxyInfo = await resolveProxyForConnection(credentials.connectionId);
} catch {
log.debug("PROXY", `Failed to resolve proxy for image provider: ${provider}`);
}
@@ -248,8 +248,12 @@ export async function POST(request) {
});
// Execute with proxy context when available, direct otherwise (#1904)
const result = await (credentials?.id
? runWithProxyContext(proxyInfo?.proxy || null, generateImage)
const result = await (credentials?.connectionId
? runWithProxyContext(proxyInfo?.proxy || null, generateImage).catch((err: any) => ({
success: false,
status: err.statusCode || 500,
error: err.message,
}))
: generateImage());
if (result.success) {

View File

@@ -157,15 +157,27 @@ test("codex failover: Object.assign patches credentials with new account", () =>
// ── Test 6: failover only triggers on 429, not other errors ──────────────────
test("codex failover: only 429 triggers account rotation", () => {
const shouldTriggerFailover = (provider: string, status: number, attempt: number, maxAttempts: number) =>
provider === "codex" && status === 429 && attempt < maxAttempts - 1;
const shouldTriggerFailover = (
provider: string,
status: number,
attempt: number,
maxAttempts: number
) => provider === "codex" && status === 429 && attempt < maxAttempts - 1;
assert.equal(shouldTriggerFailover("codex", 429, 0, 3), true, "Codex 429 attempt 0 → rotate");
assert.equal(shouldTriggerFailover("codex", 429, 1, 3), true, "Codex 429 attempt 1 → rotate");
assert.equal(shouldTriggerFailover("codex", 429, 2, 3), false, "Codex 429 last attempt → no rotate");
assert.equal(
shouldTriggerFailover("codex", 429, 2, 3),
false,
"Codex 429 last attempt → no rotate"
);
assert.equal(shouldTriggerFailover("codex", 500, 0, 3), false, "Codex 500 → no rotate");
assert.equal(shouldTriggerFailover("codex", 200, 0, 3), false, "Codex 200 → no rotate");
assert.equal(shouldTriggerFailover("openai", 429, 0, 1), false, "OpenAI 429 → no rotate (not codex)");
assert.equal(
shouldTriggerFailover("openai", 429, 0, 1),
false,
"OpenAI 429 → no rotate (not codex)"
);
});
// ── Test 7: no-credentials response stops rotation ───────────────────────────

View File

@@ -11,6 +11,7 @@ process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "image-route-test-api
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const imageRoute = await import("../../src/app/api/v1/images/generations/route.ts");
const imageEditRoute = await import("../../src/app/api/v1/images/edits/route.ts");
@@ -138,3 +139,97 @@ test("v1 image edit POST enforces disabled API key policy", async () => {
assert.equal(response.status, 403);
assert.match(body.error.message, /disabled/);
});
test("v1 image generation POST resolves proxy and executes with proxy context when credentials.connectionId exists", async () => {
// Create a connection — it gets an auto-generated id used as credentials.connectionId
const connection = await seedConnection("openai", { apiKey: "image-proxy-key" });
// Set a key-level proxy for this specific connection (id = connectionId)
await settingsDb.setProxyForLevel("key", (connection as any).id, {
type: "http",
host: "127.0.0.1",
port: 1, // intentionally unreachable — proves proxy path was taken
});
globalThis.fetch = async () => {
throw new Error("fetch should not be called — proxy fast-fail should trigger first");
};
const response = await imageRoute.POST(
new Request("http://localhost/api/v1/images/generations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "openai/gpt-image-2",
prompt: "proxy test image",
}),
})
);
assert.equal(response.status, 503);
const body = (await response.json()) as any;
assert.match(body.error.message, /unreachable/i);
});
test("v1 image generation POST executes directly when proxy resolution fails gracefully", async () => {
const connection = await seedConnection("openai", { apiKey: "image-proxy-fail-key" });
const db = core.getDbInstance();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('proxyConfig', 'keys', 'corrupt-json')"
).run();
globalThis.fetch = async (url) => {
const stringUrl = String(url);
if (stringUrl === "https://api.openai.com/v1/images/generations") {
return new Response(
JSON.stringify({ created: 123, data: [{ url: "https://cdn.example.com/proxy-fail.png" }] }),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
const response = await imageRoute.POST(
new Request("http://localhost/api/v1/images/generations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "openai/gpt-image-2",
prompt: "proxy failover image",
}),
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.data[0].url, "https://cdn.example.com/proxy-fail.png");
});
test("v1 image generation POST executes directly when credentials.connectionId is absent (authType: none)", async () => {
globalThis.fetch = async (url) => {
const stringUrl = String(url);
if (stringUrl === "http://localhost:7860/sdapi/v1/txt2img") {
return new Response(JSON.stringify({ images: ["YmFzZTY0LWltYWdl"] }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
const response = await imageRoute.POST(
new Request("http://localhost/api/v1/images/generations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "sdwebui/stable-diffusion-v1-5",
prompt: "no credentials test",
}),
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.ok(body.data, "should have image data");
});