feat(providers): declare imageToText serviceKind on major vision providers

The /dashboard/media-providers/imageToText category was empty by design:
imageToText has no backing registry and no catalog entry declared it.
Declare serviceKinds: ["llm", "imageToText"] on the 7 major vision-capable
providers (openai, anthropic, gemini, openrouter, mistral, xai, groq) so the
category lists them and the Modality Bridge ?tab=vision shortcut becomes
reachable from their provider detail pages.

"llm" is declared alongside because ProviderCard treats an EMPTY serviceKinds
as "regular LLM provider" — declaring only imageToText would silently hide the
inline Test button and the playground default (guarded by the new test).

Refs #9760
This commit is contained in:
Xiangzhe
2026-08-13 14:14:39 -03:00
parent 266e39d36d
commit 55a2af03df
3 changed files with 49 additions and 0 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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`
);
}
});