Skip to content
Closed
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
28 changes: 25 additions & 3 deletions src/providers/derive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CodexAccountMode, OcxProviderConfig } from "../types";
import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "./registry";
import type { ProviderAccessGroup } from "./free-directory";

export interface DerivedKeyLoginProvider {
label: string;
Expand Down Expand Up @@ -67,6 +68,15 @@ export interface DerivedProviderPreset {
baseUrlChoices?: Array<{ id: string; label: string; baseUrl?: string }>;
/** Immutable canonical provider config seed for the reserved canonical `openai` forward preset. */
provider?: OcxProviderConfig;
accessGroups?: readonly ProviderAccessGroup[];
supportLevel?: "supported" | "experimental" | "reference";
verification?: "official" | "primary" | "unverified";
documentationUrl?: string;
modelsUrl?: string;
discovery?: "live" | "static" | "hybrid" | "unsupported";
lastVerified?: string;
models?: string[];
liveModels?: boolean;
}

export function listRegistryEntries(): readonly ProviderRegistryEntry[] {
Expand Down Expand Up @@ -125,7 +135,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
const out: Record<string, DerivedKeyLoginProvider> = {};
for (const entry of PROVIDER_REGISTRY) {
if (entry.authKind !== "key") continue;
if (entry.authKind !== "key" || entry.supportLevel === "reference" || entry.directoryOnly) continue;
if (!entry.dashboardUrl) throw new Error(`Registry key provider missing dashboardUrl: ${entry.id}`);
out[entry.id] = {
label: entry.label,
Expand Down Expand Up @@ -165,7 +175,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
}

export function deriveInitProviders(): DerivedInitProvider[] {
return PROVIDER_REGISTRY.map(entry => ({
return PROVIDER_REGISTRY.filter(entry => entry.supportLevel !== "reference" && !entry.directoryOnly).map(entry => ({
id: entry.id,
label: formatInitLabel(entry),
adapter: entry.adapter,
Expand All @@ -192,7 +202,7 @@ export function deriveOAuthIds(): string[] {

export function deriveProviderPresets(): DerivedProviderPreset[] {
const presets = PROVIDER_REGISTRY
.filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset)
.filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length)
.map(entryToPreset);
return [...dedupePresets(presets), customPreset()];
}
Comment on lines 203 to 208

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reference-only directory entries leak into deriveProviderPresets().

The filter entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length admits any entry with a non-empty accessGroups. Since every FREE_PROVIDER_DIRECTORY entry (per src/providers/free-directory.ts Lines 145-151) always has at least one access group by construction — including pure reference entries like "agy", "blackbox", "coze", "duckduckgo-web" that have baseUrl: "", no dashboardUrl, and supportLevel: "reference" — this filter (and the dashboardPreset clause, since registry.ts sets dashboardPreset: true unconditionally for all directory-only rows at Lines 1045-1049) lets them pass through into the connectable preset list produced by entryToPreset.

This contradicts the intended behavior for this cohort ("excludes reference or directory-only entries from connectable provider outputs") and the explicit design note in free-directory.ts that consumer-web/session providers must stay reference-only and never be surfaced as something a user can connect to.

🐛 Proposed fix
 export function deriveProviderPresets(): DerivedProviderPreset[] {
   const presets = PROVIDER_REGISTRY
-    .filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length)
+    .filter(entry => entry.supportLevel !== "reference" && (entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length))
     .map(entryToPreset);
   return [...dedupePresets(presets), customPreset()];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function deriveProviderPresets(): DerivedProviderPreset[] {
const presets = PROVIDER_REGISTRY
.filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset)
.filter(entry => entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length)
.map(entryToPreset);
return [...dedupePresets(presets), customPreset()];
}
export function deriveProviderPresets(): DerivedProviderPreset[] {
const presets = PROVIDER_REGISTRY
.filter(entry => entry.supportLevel !== "reference" && (entry.featured || entry.authKind === "key" || entry.dashboardPreset || entry.accessGroups?.length))
.map(entryToPreset);
return [...dedupePresets(presets), customPreset()];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/derive.ts` around lines 203 - 208, Update
deriveProviderPresets() to exclude reference-only and directory-only registry
entries before applying the existing featured, key-auth, dashboard, or
access-group eligibility checks. Reuse the registry metadata that identifies
non-connectable/reference entries, ensuring providers such as agy, blackbox,
coze, and duckduckgo-web cannot reach entryToPreset while normal connectable
presets remain unchanged.

Expand Down Expand Up @@ -231,6 +241,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier;
if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
if (!prov.headers && seed.headers) prov.headers = { ...seed.headers };
if (prov.googleMode === undefined && seed.googleMode !== undefined) prov.googleMode = seed.googleMode;
if (prov.project === undefined && seed.project !== undefined) prov.project = seed.project;
if (prov.location === undefined && seed.location !== undefined) prov.location = seed.location;
}

export function deriveFeaturedProviderIds(): string[] {
Expand Down Expand Up @@ -270,6 +283,15 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
...(entry.keyOptional ? { keyOptional: true } : {}),
...(entry.freeTier ? { freeTier: true } : {}),
...(entry.baseUrlChoices ? { baseUrlChoices: entry.baseUrlChoices.map(c => ({ ...c })) } : {}),
...(entry.accessGroups ? { accessGroups: [...entry.accessGroups] } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark access-group presets as free in the catalog

Publishing accessGroups alone does not move these new free-directory presets into the GUI's Free tab: gui/src/components/provider-catalog/provider-presets.ts still derives tiers only from freeTier, keyOptional, local, or account shape. As a result providers whose only free signal is an access group, such as groq (recurring-or-keyless) or ai21 (signup-credit), are included in /api/provider-presets but still render under Paid. Either set the existing freeTier signal for access-group presets or update the catalog tiering to consume this field.

Useful? React with 👍 / 👎.

...(entry.supportLevel ? { supportLevel: entry.supportLevel } : {}),
...(entry.verification ? { verification: entry.verification } : {}),
...(entry.documentationUrl ? { documentationUrl: entry.documentationUrl } : {}),
...(entry.modelsUrl ? { modelsUrl: entry.modelsUrl } : {}),
...(entry.discovery ? { discovery: entry.discovery } : {}),
...(entry.lastVerified ? { lastVerified: entry.lastVerified } : {}),
...(entry.models ? { models: [...entry.models] } : {}),
...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
};
}

Expand Down
Loading
Loading