mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
duckduckgo-web returned 400 ERR_BAD_REQUEST on every request because the model
catalog advertised ids DuckDuckGo has retired from the free Duck.ai lineup
(gpt-4o-mini, gpt-5-mini, llama-4-scout, mistral-small-2501, o3-mini,
claude-3-5-haiku-20241022). duckchat/v1/chat validates `model` server-side and
rejects retired ids, and normalizeDuckDuckGoModel() defaulted to / passed through
gpt-4o-mini, so the retired id reached the wire verbatim.
Update all three id sources to the current free wire ids captured live from
duckchat/v1/models (2026-07-22): gpt-5.4-mini, gpt-5.4-nano, claude-haiku-4-5,
mistral-small-2603, tinfoil/gpt-oss-120b, tinfoil/gemma4-31b —
- executor: default gpt-4o-mini -> gpt-5.4-mini; legacy ids aliased to the
nearest current model via a lookup map; dropped the invalid gpt-5-mini
"minimal" reasoningEffort;
- freeModelCatalog.data.ts + providers/registry/duckduckgo-web: current ids.
Regression test duckduckgo-web-model-catalog-8000.test.ts asserts no retired id
ever reaches the wire and all three catalogs match the current set (RED on the
old default/passthrough + retired catalogs). Live 200 confirmation remains a
recommended VPS smoke per the plan-file.
This commit is contained in:
committed by
GitHub
parent
19c3ff51e7
commit
0f342f41fe
1
changelog.d/fixes/8000-duckduckgo-catalog.md
Normal file
1
changelog.d/fixes/8000-duckduckgo-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): refresh duckduckgo-web free model catalog to Duck.ai's current wire ids, fixing 400 ERR_BAD_REQUEST (#8000)
|
||||
@@ -154,12 +154,12 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
|
||||
{ provider: "deepseek", modelId: "deepseek-v4-pro", displayName: "DeepSeek V4 Pro", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "deepseek", tos: "ok" },
|
||||
{ provider: "deepseek", modelId: "deepseek-v4-flash", displayName: "DeepSeek V4 Flash", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "deepseek", tos: "ok" },
|
||||
{ provider: "doubao", modelId: "doubao-pro-32k", displayName: "Doubao Pro 32K", monthlyTokens: 0, creditTokens: 15000000, freeType: "one-time-initial", poolKey: "doubao", tos: "ambiguous" },
|
||||
{ provider: "duckduckgo-web", modelId: "gpt-4o-mini", displayName: "GPT-4o Mini", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "gpt-5-mini", displayName: "GPT-5 Mini", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "claude-3-5-haiku-20241022", displayName: "Claude 3.5 Haiku", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "llama-4-scout", displayName: "Llama 4 Scout", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "mistral-small-2501", displayName: "Mistral Small", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "o3-mini", displayName: "O3 Mini", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "gpt-5.4-mini", displayName: "GPT-5.4 Mini", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "gpt-5.4-nano", displayName: "GPT-5.4 Nano", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "mistral-small-2603", displayName: "Mistral Small 4", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "tinfoil/gpt-oss-120b", displayName: "gpt-oss 120B", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "duckduckgo-web", modelId: "tinfoil/gemma4-31b", displayName: "Gemma 4 31B", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "duckduckgo-web", tos: "avoid" },
|
||||
{ provider: "felo-web", modelId: "felo-chat", displayName: "Felo Chat", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "felo-web", tos: "avoid" },
|
||||
{ provider: "felo-web", modelId: "felo-search", displayName: "Felo Search", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "felo-web", tos: "avoid" },
|
||||
{ provider: "felo-web", modelId: "felo-scholar", displayName: "Felo Scholar", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "felo-web", tos: "avoid" },
|
||||
|
||||
@@ -8,12 +8,13 @@ export const duckduckgo_webProvider: RegistryEntry = {
|
||||
baseUrl: "https://duckduckgo.com/duckchat/v1/chat",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
// #8000: current Duck.ai free lineup — wire ids per duckchat/v1/models (2026-07-22).
|
||||
models: [
|
||||
{ id: "gpt-4o-mini", name: "GPT-4o Mini", toolCalling: false },
|
||||
{ id: "gpt-5-mini", name: "GPT-5 Mini", toolCalling: false },
|
||||
{ id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku", toolCalling: false },
|
||||
{ id: "llama-4-scout", name: "Llama 4 Scout", toolCalling: false },
|
||||
{ id: "mistral-small-2501", name: "Mistral Small", toolCalling: false },
|
||||
{ id: "o3-mini", name: "O3 Mini", toolCalling: false },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini", toolCalling: false },
|
||||
{ id: "gpt-5.4-nano", name: "GPT-5.4 Nano", toolCalling: false },
|
||||
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5", toolCalling: false },
|
||||
{ id: "mistral-small-2603", name: "Mistral Small 4", toolCalling: false },
|
||||
{ id: "tinfoil/gpt-oss-120b", name: "gpt-oss 120B", toolCalling: false },
|
||||
{ id: "tinfoil/gemma4-31b", name: "Gemma 4 31B", toolCalling: false },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -226,18 +226,37 @@ function mergeHeadersCaseInsensitive(
|
||||
return merged;
|
||||
}
|
||||
|
||||
function normalizeDuckDuckGoModel(model: string | undefined): string {
|
||||
if (!model) return "gpt-4o-mini";
|
||||
/**
|
||||
* #8000: DuckDuckGo's free Duck.ai lineup churns and the catalog fell behind. Map every
|
||||
* retired id OmniRoute historically advertised to the current wire id served by
|
||||
* `duckchat/v1/models` (captured 2026-07-22) — a retired/unknown `model` yields a 400
|
||||
* `ERR_BAD_REQUEST` from `duckchat/v1/chat`. Current free wire ids: gpt-5.4-mini,
|
||||
* gpt-5.4-nano, claude-haiku-4-5, mistral-small-2603, tinfoil/gpt-oss-120b, tinfoil/gemma4-31b.
|
||||
*/
|
||||
export const DUCKDUCKGO_DEFAULT_MODEL = "gpt-5.4-mini";
|
||||
export const DUCKDUCKGO_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
||||
// retired OpenAI ids → current GPT-5.4 free tier
|
||||
"gpt-4o-mini": "gpt-5.4-mini",
|
||||
"gpt-5-mini": "gpt-5.4-mini",
|
||||
"o3-mini": "gpt-5.4-nano",
|
||||
// retired Llama (dropped from Duck.ai free) → nearest general free model
|
||||
"llama-4-scout": "gpt-5.4-mini",
|
||||
// renamed/versioned ids
|
||||
"claude-3-5-haiku-20241022": "claude-haiku-4-5",
|
||||
"mistral-small-2501": "mistral-small-2603",
|
||||
"gpt-oss-120b": "tinfoil/gpt-oss-120b",
|
||||
"gemma4-31b": "tinfoil/gemma4-31b",
|
||||
};
|
||||
|
||||
export function normalizeDuckDuckGoModel(model: string | undefined): string {
|
||||
if (!model) return DUCKDUCKGO_DEFAULT_MODEL;
|
||||
const clean = model.startsWith("duckduckgo-web/") ? model.slice("duckduckgo-web/".length) : model;
|
||||
if (clean === "claude-3-5-haiku-20241022") return "claude-haiku-4-5";
|
||||
if (clean === "llama-4-scout") return "meta-llama/Llama-4-Scout-17B-16E-Instruct";
|
||||
if (clean === "mistral-small-2501") return "mistral-small-2603";
|
||||
if (clean === "gpt-oss-120b") return "tinfoil/gpt-oss-120b";
|
||||
return clean;
|
||||
return DUCKDUCKGO_MODEL_ALIASES[clean] ?? clean;
|
||||
}
|
||||
|
||||
function getDuckDuckGoModelCapabilities(model: string): DuckDuckGoModelCapabilities {
|
||||
if (model === "gpt-5-mini") return { reasoningEffort: "minimal" };
|
||||
// Per duckchat/v1/models (2026-07-22): claude-haiku-4-5 and gpt-oss-120b take a "low"
|
||||
// reasoningEffort on the free tier; the others omit it (duck.ai applies its own default).
|
||||
if (model === "claude-haiku-4-5") return { reasoningEffort: "low" };
|
||||
if (model === "tinfoil/gpt-oss-120b") return { reasoningEffort: "low" };
|
||||
return { reasoningEffort: null };
|
||||
|
||||
72
tests/unit/duckduckgo-web-model-catalog-8000.test.ts
Normal file
72
tests/unit/duckduckgo-web-model-catalog-8000.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
normalizeDuckDuckGoModel,
|
||||
DUCKDUCKGO_DEFAULT_MODEL,
|
||||
} from "../../open-sse/executors/duckduckgo-web.ts";
|
||||
import { duckduckgo_webProvider } from "../../open-sse/config/providers/registry/duckduckgo-web/index.ts";
|
||||
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";
|
||||
|
||||
// #8000 — the current free Duck.ai lineup, wire ids captured live from
|
||||
// duckchat/v1/models (2026-07-22). A retired/unknown model id is rejected by
|
||||
// duckchat/v1/chat with 400 ERR_BAD_REQUEST, which is the exact reported symptom.
|
||||
const CURRENT_FREE_IDS = new Set([
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.4-nano",
|
||||
"claude-haiku-4-5",
|
||||
"mistral-small-2603",
|
||||
"tinfoil/gpt-oss-120b",
|
||||
"tinfoil/gemma4-31b",
|
||||
]);
|
||||
|
||||
// Ids OmniRoute historically advertised that Duck.ai has since retired.
|
||||
const RETIRED_IDS = [
|
||||
"gpt-4o-mini",
|
||||
"gpt-5-mini",
|
||||
"o3-mini",
|
||||
"llama-4-scout",
|
||||
"claude-3-5-haiku-20241022",
|
||||
"mistral-small-2501",
|
||||
];
|
||||
|
||||
test("#8000: default model is a current free wire id, not retired gpt-4o-mini", () => {
|
||||
assert.equal(DUCKDUCKGO_DEFAULT_MODEL, "gpt-5.4-mini");
|
||||
assert.notEqual(normalizeDuckDuckGoModel(undefined), "gpt-4o-mini");
|
||||
assert.ok(CURRENT_FREE_IDS.has(normalizeDuckDuckGoModel(undefined)));
|
||||
});
|
||||
|
||||
test("#8000: every retired id normalizes to a current wire id (never passes through)", () => {
|
||||
for (const retired of RETIRED_IDS) {
|
||||
const out = normalizeDuckDuckGoModel(retired);
|
||||
assert.ok(CURRENT_FREE_IDS.has(out), `${retired} → ${out} must be a current free id`);
|
||||
assert.notEqual(out, retired, `retired ${retired} must not reach the wire unchanged`);
|
||||
}
|
||||
// the `duckduckgo-web/` routing prefix is stripped before aliasing
|
||||
assert.ok(CURRENT_FREE_IDS.has(normalizeDuckDuckGoModel("duckduckgo-web/gpt-4o-mini")));
|
||||
assert.equal(normalizeDuckDuckGoModel("duckduckgo-web/gpt-5.4-nano"), "gpt-5.4-nano");
|
||||
});
|
||||
|
||||
test("#8000: current wire ids pass through unchanged", () => {
|
||||
for (const id of CURRENT_FREE_IDS) {
|
||||
assert.equal(normalizeDuckDuckGoModel(id), id);
|
||||
}
|
||||
});
|
||||
|
||||
test("#8000: provider registry advertises exactly the current wire ids", () => {
|
||||
const ids = duckduckgo_webProvider.models.map((m) => m.id);
|
||||
assert.deepEqual(new Set(ids), CURRENT_FREE_IDS);
|
||||
for (const retired of RETIRED_IDS) {
|
||||
assert.ok(!ids.includes(retired), `registry must drop retired id ${retired}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("#8000: free-model catalog advertises exactly the current wire ids", () => {
|
||||
const ids = FREE_MODEL_BUDGETS.filter((e) => e.provider === "duckduckgo-web").map(
|
||||
(e) => e.modelId
|
||||
);
|
||||
assert.deepEqual(new Set(ids), CURRENT_FREE_IDS);
|
||||
for (const retired of RETIRED_IDS) {
|
||||
assert.ok(!ids.includes(retired), `catalog must drop retired id ${retired}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user