Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- **fix(gamification):** badge-unlock events no longer re-fire on every request — the "already unlocked?" guard used `getBadges()`, which INNER-JOINs `badge_definitions` (empty until seeded), so it always reported "not earned" and re-emitted `events.badge_unlocked` per request. Added a `hasBadge()` helper that reads `user_badges` directly, so dedup is correct regardless of whether definitions are seeded. ([#3472](https://github.com/diegosouzapw/OmniRoute/issues/3472))
- **fix(routing):** the `auto` model keyword now works on the Codex `/v1/responses` path — `resolveResponsesApiModel` rewrote the bare `auto` keyword to `codex/auto`, which ChatGPT rejects (`The 'auto' model is not supported when using Codex with a ChatGPT account`). `auto` (OmniRoute's zero-config auto-routing keyword) now passes through untouched so combo routing handles it. ([#3509](https://github.com/diegosouzapw/OmniRoute/issues/3509))
- **fix(cli-tools):** saving the OpenCode/CLI tool config no longer 400s in cloud mode — every CLI tool card posts `apiKey: null` (the real key is resolved server-side from `keyId`), but `guideSettingsSaveSchema` used `z.string().optional()`, which rejects `null`. The schema now normalizes `null` → `undefined`, so the save succeeds and the `keyId`/default path is used. ([#3552](https://github.com/diegosouzapw/OmniRoute/issues/3552))
- **fix(catalog):** PublicAI is no longer miscatalogued as keyless/free — it requires an API key (registry `authType:"apikey"`; signup grants a one-time credit, then it bills). The three PublicAI models moved from `freeType:"keyless"` (which could pick them into the no-auth pool and dispatch with no `Authorization` header) to `"one-time-initial"`, and the provider's `hasFree` flag is now `false` — matching `freeTierCatalog.ts`, which already excluded publicai. ([#3558](https://github.com/diegosouzapw/OmniRoute/issues/3558))

---

Expand Down
6 changes: 3 additions & 3 deletions open-sse/config/freeModelCatalog.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,9 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "nscale", modelId: "openai/gpt-oss-20b", displayName: "openai/gpt-oss-20b", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
{ provider: "nscale", modelId: "meta-llama/Llama-4-Scout-17B-16E-Instruct", displayName: "meta-llama/Llama-4-Scout-17B-16E-Instruct", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
{ provider: "nscale", modelId: "meta-llama/Llama-3.3-70B-Instruct", displayName: "meta-llama/Llama-3.3-70B-Instruct", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
{ provider: "publicai", modelId: "swiss-ai/apertus-70b-instruct", displayName: "swiss-ai/apertus-70b-instruct", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "publicai", tos: "caution" },
{ provider: "publicai", modelId: "aisingapore/Qwen-SEA-LION-v4-32B-IT", displayName: "aisingapore/Qwen-SEA-LION-v4-32B-IT", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "publicai", tos: "caution" },
{ provider: "publicai", modelId: "allenai/Olmo-3-32B-Think", displayName: "allenai/Olmo-3-32B-Think", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "publicai", tos: "caution" },
{ provider: "publicai", modelId: "swiss-ai/apertus-70b-instruct", displayName: "swiss-ai/apertus-70b-instruct", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "publicai", tos: "caution" },
{ provider: "publicai", modelId: "aisingapore/Qwen-SEA-LION-v4-32B-IT", displayName: "aisingapore/Qwen-SEA-LION-v4-32B-IT", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "publicai", tos: "caution" },
{ provider: "publicai", modelId: "allenai/Olmo-3-32B-Think", displayName: "allenai/Olmo-3-32B-Think", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "publicai", tos: "caution" },
Comment on lines +410 to +412

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since publicai is now marked as hasFree: false in providers.ts and has both monthlyTokens: 0 and creditTokens: 0, these models are no longer part of any active free tier or budget. Keeping them in FREE_MODEL_BUDGETS (which is specifically for free model budgets) is inconsistent and could lead to dead entries or unexpected behavior in the UI/routing logic.

Consider removing these three publicai entries from FREE_MODEL_BUDGETS entirely instead of reclassifying them to one-time-initial with zero tokens.

{ provider: "morph", modelId: "morph-v3-large", displayName: "morph-v3-large", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "caution" },
{ provider: "morph", modelId: "morph-v3-fast", displayName: "morph-v3-fast", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "caution" },
{ provider: "featherless-ai", modelId: "featherless-ai/Qwerky-72B", displayName: "featherless-ai/Qwerky-72B", monthlyTokens: 0, creditTokens: 0, freeType: "discontinued", poolKey: "featherless-ai", tos: "avoid" },
Expand Down
6 changes: 4 additions & 2 deletions src/shared/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1434,8 +1434,10 @@ export const APIKEY_PROVIDERS = {
color: "#059669",
textIcon: "PA",
website: "https://publicai.co",
hasFree: true,
freeNote: "Free community inference tier for testing",
// #3558: PublicAI requires an API key (registry authType:"apikey"); signup grants a
// one-time credit, then it bills per-model. It is NOT a keyless/free tier.
hasFree: false,
freeNote: "Requires an API key — one-time signup credit, then paid",
},
moonshot: {
id: "moonshot",
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/publicai-not-keyless-3558.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";

// Regression for #3558: PublicAI was catalogued with freeType:"keyless" (in the RECURRING set,
// so it surfaced as keyless-free and could be picked into the no-auth pool with no Authorization
// header). But the provider registry requires a key (authType:"apikey"), and freeTierCatalog.ts
// already excludes publicai from recurring free budgets — an internal inconsistency. PublicAI
// grants a one-time signup credit then bills, so it must NOT be "keyless".

test("#3558 PublicAI has no freeType:keyless catalog entries", () => {
const keyless = FREE_MODEL_BUDGETS.filter(
(b) => b.provider === "publicai" && b.freeType === "keyless"
);
assert.equal(keyless.length, 0, `publicai must not be keyless; found ${keyless.length}`);
});

test("#3558 PublicAI catalog entries are classified one-time-initial", () => {
const entries = FREE_MODEL_BUDGETS.filter((b) => b.provider === "publicai");
assert.ok(entries.length > 0, "publicai still has catalog entries");
for (const e of entries) {
assert.equal(e.freeType, "one-time-initial", `${e.modelId} should be one-time-initial`);
}
});
Comment on lines +1 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the project uses Vitest (as indicated by vitest.config.ts in the root), it is highly recommended to use vitest for writing unit tests instead of Node's built-in node:test and node:assert. This ensures consistent test execution, reporting, and coverage analysis across the codebase.

If you decide to remove the publicai entries from FREE_MODEL_BUDGETS entirely (as suggested in the other comment), this test can be simplified to assert that FREE_MODEL_BUDGETS contains zero entries for publicai. Below is the suggested implementation using Vitest.

Suggested change
import test from "node:test";
import assert from "node:assert/strict";
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";
// Regression for #3558: PublicAI was catalogued with freeType:"keyless" (in the RECURRING set,
// so it surfaced as keyless-free and could be picked into the no-auth pool with no Authorization
// header). But the provider registry requires a key (authType:"apikey"), and freeTierCatalog.ts
// already excludes publicai from recurring free budgets — an internal inconsistency. PublicAI
// grants a one-time signup credit then bills, so it must NOT be "keyless".
test("#3558 PublicAI has no freeType:keyless catalog entries", () => {
const keyless = FREE_MODEL_BUDGETS.filter(
(b) => b.provider === "publicai" && b.freeType === "keyless"
);
assert.equal(keyless.length, 0, `publicai must not be keyless; found ${keyless.length}`);
});
test("#3558 PublicAI catalog entries are classified one-time-initial", () => {
const entries = FREE_MODEL_BUDGETS.filter((b) => b.provider === "publicai");
assert.ok(entries.length > 0, "publicai still has catalog entries");
for (const e of entries) {
assert.equal(e.freeType, "one-time-initial", `${e.modelId} should be one-time-initial`);
}
});
import { test, expect } from "vitest";
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";
// Regression for #3558: PublicAI requires an API key and is not part of the free model catalog.
test("#3558 PublicAI has no entries in the free model catalog", () => {
const entries = FREE_MODEL_BUDGETS.filter((b) => b.provider === "publicai");
expect(entries.length).toBe(0);
});
References
  1. The repository uses Vitest as its official testing framework (as indicated by vitest.config.ts and Vitest files references in the style guide). Unit tests should use Vitest APIs rather than node:test. (link)