diff --git a/src/shared/constants/providers/apikey/frontier-labs.ts b/src/shared/constants/providers/apikey/frontier-labs.ts index 60139bde84..cd1f98975a 100644 --- a/src/shared/constants/providers/apikey/frontier-labs.ts +++ b/src/shared/constants/providers/apikey/frontier-labs.ts @@ -11,6 +11,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com", + serviceKinds: ["llm", "imageToText"], }, reka: { id: "reka", @@ -52,6 +53,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { color: "#D97757", textIcon: "AN", website: "https://platform.claude.com", + serviceKinds: ["llm", "imageToText"], }, gemini: { id: "gemini", @@ -64,6 +66,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { hasFree: true, freeNote: "Free tier available through Google AI Studio; current per-model quotas and regional limits apply", + serviceKinds: ["llm", "imageToText"], }, groq: { id: "groq", @@ -75,6 +78,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { website: "https://groq.com", hasFree: true, freeNote: "Free tier: 30 RPM / 14.4K RPD — no credit card", + serviceKinds: ["llm", "imageToText"], }, blackbox: { id: "blackbox", @@ -96,6 +100,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { color: "#1DA1F2", textIcon: "XA", website: "https://x.ai", + serviceKinds: ["llm", "imageToText"], }, mistral: { id: "mistral", @@ -107,6 +112,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { website: "https://mistral.ai", hasFree: true, freeNote: "Free Experiment tier: rate-limited access to all models, no credit card required", + serviceKinds: ["llm", "imageToText"], }, perplexity: { id: "perplexity", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 860117943d..15b8fb3edc 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -82,6 +82,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = { website: "https://openrouter.ai", hasFree: true, freeNote: "Free models at $0/token with :free suffix - 20 RPM / 200 RPD", + serviceKinds: ["llm", "imageToText"], }, requesty: { id: "requesty", diff --git a/tests/unit/imagetotext-service-kinds.test.ts b/tests/unit/imagetotext-service-kinds.test.ts new file mode 100644 index 0000000000..9a486586a0 --- /dev/null +++ b/tests/unit/imagetotext-service-kinds.test.ts @@ -0,0 +1,42 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { AI_PROVIDERS } from "../../src/shared/constants/providers.ts"; +import { resolveProviderServiceKinds } from "../../open-sse/config/mediaServiceKinds.ts"; + +/** + * The Image-to-Text category (/dashboard/media-providers/imageToText) fills from + * providers whose resolved serviceKinds include "imageToText". It has no backing + * registry, so major vision-capable providers must declare it explicitly. + */ +const IMAGE_TO_TEXT_PROVIDERS = [ + "openai", + "anthropic", + "gemini", + "openrouter", + "mistral", + "xai", + "groq", +] as const; + +test("major vision providers declare the imageToText serviceKind", () => { + for (const id of IMAGE_TO_TEXT_PROVIDERS) { + const provider = AI_PROVIDERS[id] as { serviceKinds?: string[] } | undefined; + assert.ok(provider, `provider "${id}" missing from AI_PROVIDERS`); + const kinds = resolveProviderServiceKinds(id, provider.serviceKinds); + assert.ok(kinds.includes("imageToText"), `"${id}" must resolve the imageToText serviceKind`); + } +}); + +test("declaring imageToText keeps the llm kind (inline Test button + playground default)", () => { + // ProviderCard treats an EMPTY serviceKinds as "regular LLM provider"; once a + // provider declares any kind, "llm" must be declared too or the Test button + // and the playground default silently disappear. + for (const id of IMAGE_TO_TEXT_PROVIDERS) { + const provider = AI_PROVIDERS[id] as { serviceKinds?: string[] }; + assert.ok( + (provider.serviceKinds ?? []).includes("llm"), + `"${id}" declares serviceKinds without "llm" — this hides the inline Test button` + ); + } +});