fix: improve provider routing and assignments
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterConnectionsForModel } from "../../src/sse/services/auth.js";
|
||||
|
||||
const connections = [
|
||||
{ id: "flash-1", providerSpecificData: { freebuffModel: "deepseek/deepseek-v4-flash" } },
|
||||
{ id: "mimo-1", providerSpecificData: { freebuffModel: "mimo/mimo-v2.5" } },
|
||||
{ id: "unassigned", providerSpecificData: {} },
|
||||
];
|
||||
|
||||
describe("Freebuff strict model assignment", () => {
|
||||
it("keeps only accounts assigned to the requested model", () => {
|
||||
const result = filterConnectionsForModel("freebuff", connections, "mimo/mimo-v2.5", {
|
||||
providerStrategies: { freebuff: { strictModelAssignment: true } },
|
||||
});
|
||||
|
||||
expect(result.map((connection) => connection.id)).toEqual(["mimo-1"]);
|
||||
});
|
||||
|
||||
it("excludes unassigned accounts when strict mode is enabled", () => {
|
||||
const result = filterConnectionsForModel("freebuff", connections, "deepseek/deepseek-v4-flash", {
|
||||
providerStrategies: { freebuff: { strictModelAssignment: true } },
|
||||
});
|
||||
|
||||
expect(result.map((connection) => connection.id)).toEqual(["flash-1"]);
|
||||
});
|
||||
|
||||
it("preserves the existing pool when strict mode is disabled", () => {
|
||||
expect(filterConnectionsForModel("freebuff", connections, "mimo/mimo-v2.5", {})).toBe(connections);
|
||||
});
|
||||
|
||||
it("does not affect other providers when their toggle is off", () => {
|
||||
expect(filterConnectionsForModel("codex", connections, "mimo/mimo-v2.5", {
|
||||
providerStrategies: {},
|
||||
})).toBe(connections);
|
||||
});
|
||||
});
|
||||
@@ -207,6 +207,43 @@ describe("freebuff executor wire shape", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
expect(ex.buildUrl()).toBe("https://www.codebuff.com/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("injects the end_turn tool into any tool-calling request (backend foreign_toolset gate)", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [{ type: "function", function: { name: "read_file", description: "read" } }],
|
||||
};
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
const names = out.tools.map((t) => t.function.name);
|
||||
expect(names).toContain("read_file");
|
||||
expect(names).toContain("end_turn");
|
||||
expect(out.tools[out.tools.length - 1].function).toMatchObject({
|
||||
name: "end_turn",
|
||||
description: "Signal the end of the current task.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject end_turn when the request has no tools", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: "deepseek/deepseek-v4-flash", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
expect(out.tools).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not duplicate end_turn when the caller already declared it", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const endTurn = { type: "function", function: { name: "end_turn", description: "Signal the end of the current task.", parameters: { type: "object", properties: {} } } };
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [endTurn],
|
||||
};
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
expect(out.tools).toHaveLength(1);
|
||||
expect(out.tools[0].function.name).toBe("end_turn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff session pre-flight", () => {
|
||||
@@ -301,11 +338,11 @@ describe("freebuff free-tier system marker", () => {
|
||||
|
||||
describe("freebuff run registration", () => {
|
||||
it("maps freebuff models to their root free agent ids", () => {
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-flash")).toBe("base2-free-deepseek-flash");
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-pro")).toBe("base2-free-deepseek");
|
||||
expect(rootAgentIdForModel("mimo/mimo-v2.5")).toBe("base2-free-mimo");
|
||||
expect(rootAgentIdForModel("minimax/minimax-m3")).toBe("base2-free-minimax-m3");
|
||||
expect(rootAgentIdForModel("openai/gpt-5.6-luna")).toBe("base2-free-luna");
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-flash")).toBe("base3-free-deepseek-flash");
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-pro")).toBe("base3-free-deepseek");
|
||||
expect(rootAgentIdForModel("mimo/mimo-v2.5")).toBe("base3-free-mimo");
|
||||
expect(rootAgentIdForModel("minimax/minimax-m3")).toBe("base3-free-minimax-m3");
|
||||
expect(rootAgentIdForModel("openai/gpt-5.6-luna")).toBe("base3-free-luna");
|
||||
expect(rootAgentIdForModel("some/unknown-model")).toBe("base2-free");
|
||||
});
|
||||
|
||||
@@ -320,7 +357,7 @@ describe("freebuff run registration", () => {
|
||||
expect(opts.headers.Authorization).toBe("Bearer tok-1");
|
||||
const payload = JSON.parse(opts.body);
|
||||
expect(payload.action).toBe("START");
|
||||
expect(payload.agentId).toBe("base2-free-deepseek-flash");
|
||||
expect(payload.agentId).toBe("base3-free-deepseek-flash");
|
||||
expect(payload.ancestorRunIds).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -586,3 +623,28 @@ describe("freebuff executor execute", () => {
|
||||
expect(JSON.parse(finishes[0][1].body).status).toBe("cancelled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff executor parseError", () => {
|
||||
it("explains a 404 'No endpoints found' as the toolset gate, not a credential problem", async () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const res = jsonResponse(
|
||||
{ error: { message: "No endpoints found for deepseek/deepseek-v4-flash.", code: 404, type: null, param: null } },
|
||||
{ status: 404, ok: false },
|
||||
);
|
||||
const parsed = await ex.parseError(res, JSON.stringify({ error: { message: "No endpoints found for deepseek/deepseek-v4-flash.", code: 404 } }));
|
||||
|
||||
expect(parsed.status).toBe(404);
|
||||
expect(parsed.message).toMatch(/end_turn/i);
|
||||
expect(parsed.message).not.toMatch(/credential/i);
|
||||
expect(parsed.resetsAtMs).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("passes other statuses through untouched", async () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const res = jsonResponse({ error: "bad" }, { status: 500, ok: false });
|
||||
const parsed = await ex.parseError(res, JSON.stringify({ error: "bad" }));
|
||||
expect(parsed.status).toBe(500);
|
||||
expect(parsed.message).toContain("bad");
|
||||
expect(parsed.resetsAtMs).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,3 +48,31 @@ describe("classifyOAuthProbeResult (grok-cli)", () => {
|
||||
expect(r).toEqual({ valid: true, error: null, soft: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyOAuthProbeResult (freebuff)", () => {
|
||||
const FREEBUFF_PROBE = {
|
||||
acceptStatuses: [403, 404],
|
||||
softFailMessage: { 403: "gated" },
|
||||
};
|
||||
|
||||
it("treats 404 (no session row) as silent success", () => {
|
||||
const r = classifyOAuthProbeResult({ ok: false, status: 404 }, FREEBUFF_PROBE, "");
|
||||
expect(r).toEqual({ valid: true, error: null, soft: false });
|
||||
});
|
||||
|
||||
it("treats 403 (region/account gate) as soft success", () => {
|
||||
const r = classifyOAuthProbeResult(
|
||||
{ ok: false, status: 403 },
|
||||
FREEBUFF_PROBE,
|
||||
JSON.stringify({ status: "country_blocked", countryCode: "XX" }),
|
||||
);
|
||||
expect(r.valid).toBe(true);
|
||||
expect(r.soft).toBe(true);
|
||||
expect(r.error).toMatch(/gated/);
|
||||
});
|
||||
|
||||
it("treats 401 as hard auth failure", () => {
|
||||
const r = classifyOAuthProbeResult({ ok: false, status: 401 }, FREEBUFF_PROBE, "unauthorized");
|
||||
expect(r).toEqual({ valid: false, error: "Token invalid or revoked", soft: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
pruneExpired,
|
||||
resetPoolFitness,
|
||||
} from "open-sse/services/proxyPoolFitness.js";
|
||||
import { pickProxyPoolId } from "../../src/lib/network/connectionProxy.js";
|
||||
|
||||
describe("proxy pool fitness registry", () => {
|
||||
beforeEach(() => resetPoolFitness());
|
||||
@@ -39,6 +40,30 @@ describe("proxy pool fitness registry", () => {
|
||||
expect(snap.p1["fb::m2"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a pool when every smart candidate is unfit", () => {
|
||||
markPoolUnfit("p1", "freebuff::openai/gpt-5.6-luna", Date.now() + 60_000, "limited_ip");
|
||||
markPoolUnfit("p2", "freebuff::openai/gpt-5.6-luna", Date.now() + 60_000, "limited_ip");
|
||||
|
||||
expect(pickProxyPoolId(
|
||||
["p1", "p2"],
|
||||
"smart",
|
||||
"freebuff",
|
||||
{ scope: "freebuff::openai/gpt-5.6-luna" },
|
||||
)).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves fail-open smart fallback for non-Freebuff providers", () => {
|
||||
markPoolUnfit("p1", "opencode::sonnet-4.6", Date.now() + 60_000, "ip-limit");
|
||||
markPoolUnfit("p2", "opencode::sonnet-4.6", Date.now() + 60_000, "ip-limit");
|
||||
|
||||
expect(pickProxyPoolId(
|
||||
["p1", "p2"],
|
||||
"smart",
|
||||
"opencode",
|
||||
{ scope: "opencode::sonnet-4.6" },
|
||||
)).toBe("p1");
|
||||
});
|
||||
|
||||
it("clear per scope, clear-all per provider, clear-all global, pruneExpired", () => {
|
||||
markPoolUnfit("p1", "freebuff::m1", Date.now() + 60_000);
|
||||
markPoolUnfit("p1", "kiro::m3", Date.now() + 60_000);
|
||||
|
||||
Reference in New Issue
Block a user