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
14 changes: 10 additions & 4 deletions open-sse/config/mediaServiceKinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
* derives membership from here instead of duplicating it by hand, so adding a
* provider to a registry automatically surfaces it — no second edit, no drift.
*
* Kinds without a backing registry (imageToText, webSearch, webFetch, llm) are
* still declared explicitly via `serviceKinds` on the provider entry; callers
* union the two sources.
* `imageToText` is additionally derived from `OCR_PROVIDERS` (see
* `resolveProviderServiceKinds`): a provider registered in the OCR registry gets
* `imageToText` for free, no manual `serviceKinds` edit needed. Kinds without any
* backing registry (webSearch, webFetch, llm) are still declared explicitly via
* `serviceKinds` on the provider entry; callers union declared + derived sources.
*/
import { AUDIO_TRANSCRIPTION_PROVIDERS, AUDIO_SPEECH_PROVIDERS } from "./audioRegistry.ts";
import { VIDEO_PROVIDERS } from "./videoRegistry.ts";
Expand Down Expand Up @@ -58,13 +60,17 @@ export function getRegistryMediaKinds(providerId: string): RegistryMediaKind[] {

/**
* Full set of serviceKinds for a provider: the explicitly declared ones (llm,
* web*, imageToText) unioned with the media kinds derived from the registries.
* web*, imageToText) unioned with the media kinds derived from the registries,
* plus `imageToText` derived from the OCR registry when not already declared.
*/
export function resolveProviderServiceKinds(
providerId: string,
declared: readonly string[] | undefined
): string[] {
const set = new Set<string>(declared ?? []);
for (const kind of getRegistryMediaKinds(providerId)) set.add(kind);
if (Object.prototype.hasOwnProperty.call(OCR_PROVIDERS, providerId)) {
set.add("imageToText");
}
return [...set];
}
4 changes: 4 additions & 0 deletions src/shared/constants/providers/apikey/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,10 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
"No free tier as of 2026 — Chutes moved to pay-as-you-go (free Early Access ended 2026-03).",
authHint: "Bearer API key for the Chutes OpenAI-compatible gateway.",
passthroughModels: true,
// dots.ocr (rednote-hilab/dots.ocr) is served via Chutes discovery — no static
// model entry needed (passthroughModels). Declare imageToText alongside llm
// (declaring serviceKinds means "llm" must be explicit too, see #10275).
serviceKinds: ["llm", "imageToText"],
},
// Factory AI ("Factory Droids") subscription gateway — the same backend the
// local `droid` CLI shells into, exposed here as an OpenAI-compatible HTTP
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/imagetotext-derivation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveProviderServiceKinds } from "../../open-sse/config/mediaServiceKinds.ts";
import { AI_PROVIDERS } from "../../src/shared/constants/providers.ts";

test("OCR-registry providers derive imageToText without manual declaration", () => {
assert.ok(resolveProviderServiceKinds("mistral", undefined).includes("imageToText"));
assert.ok(
resolveProviderServiceKinds("azure-document-intelligence", undefined).includes("imageToText")
);
});

test("non-OCR providers do not gain imageToText implicitly", () => {
assert.ok(!resolveProviderServiceKinds("groq", undefined).includes("imageToText"));
});

test("chutes declares llm + imageToText (dots.ocr seed, served via passthrough discovery)", () => {
const kinds = resolveProviderServiceKinds("chutes", AI_PROVIDERS.chutes.serviceKinds);
assert.ok(kinds.includes("imageToText"));
assert.ok(kinds.includes("llm"));
});
Loading