summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/listModels.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/openai-stream/src/listModels.ts')
-rw-r--r--packages/openai-stream/src/listModels.ts114
1 files changed, 68 insertions, 46 deletions
diff --git a/packages/openai-stream/src/listModels.ts b/packages/openai-stream/src/listModels.ts
index 8a76c67..df116b0 100644
--- a/packages/openai-stream/src/listModels.ts
+++ b/packages/openai-stream/src/listModels.ts
@@ -12,69 +12,91 @@ import type { FetchLike } from "@dispatch/trace-replay";
*/
interface OpenAIModelEntry {
- readonly id: string;
- readonly context_length?: number;
- readonly context_window?: number;
- readonly max_context_length?: number;
- readonly max_tokens?: number;
+ readonly id: string;
+ readonly context_length?: number;
+ readonly context_window?: number;
+ readonly max_context_length?: number;
+ readonly max_tokens?: number;
}
interface OpenAIModelListResponse {
- readonly data: readonly OpenAIModelEntry[];
+ readonly data: readonly OpenAIModelEntry[];
+}
+
+/**
+ * Whether a model id is vision-capable (can natively accept image input).
+ *
+ * The OpenAI-compatible `/models` endpoint does not reliably report image
+ * capabilities, so this is a hardcoded heuristic by model id: the Umans Kimi
+ * (`umans-kimi-k2.7`) and Umans Qwen (`umans-qwen3.6-35b-a3b`) models are
+ * vision-capable; all others are treated as non-vision. This is the single
+ * source of truth — the orchestrator's vision handoff and the `consult_vision`
+ * tool both consult the `ModelInfo.vision` flag this sets, so adding a model
+ * here enables vision everywhere. Pure: id → boolean, no I/O.
+ *
+ * (When an endpoint gains reliable vision reporting, this can be replaced with
+ * a real capability check without changing callers.)
+ */
+export function isVisionModelId(id: string): boolean {
+ const lower = id.toLowerCase();
+ return lower.includes("umans-kimi") || lower.includes("umans-qwen");
}
/**
* Pure mapping: raw OpenAI-compatible model list → ModelInfo[].
- * Extracts `contextWindow` from common field names (providers vary).
- * Extracted for direct unit testing with no I/O.
+ * Extracts `contextWindow` from common field names (providers vary) and
+ * detects vision capability via {@link isVisionModelId}. Extracted for direct
+ * unit testing with no I/O.
*/
export function parseModelList(data: readonly OpenAIModelEntry[]): readonly ModelInfo[] {
- return data.map((entry) => {
- const contextWindow =
- entry.context_length ?? entry.context_window ?? entry.max_context_length ?? entry.max_tokens;
- return {
- id: entry.id,
- ...(contextWindow !== undefined ? { contextWindow } : {}),
- };
- });
+ return data.map((entry) => {
+ const contextWindow =
+ entry.context_length ?? entry.context_window ?? entry.max_context_length ?? entry.max_tokens;
+ const vision = isVisionModelId(entry.id);
+ return {
+ id: entry.id,
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
+ ...(vision ? { vision } : {}),
+ };
+ });
}
export interface ListModelsConfig {
- readonly baseURL: string;
- readonly apiKey: string;
- readonly fetchFn?: FetchLike;
- readonly providerId: string;
+ readonly baseURL: string;
+ readonly apiKey: string;
+ readonly fetchFn?: FetchLike;
+ readonly providerId: string;
}
export async function listModels(config: ListModelsConfig): Promise<readonly ModelInfo[]> {
- const effectiveFetch: FetchLike = config.fetchFn ?? fetch;
- const url = `${config.baseURL}/models`;
+ const effectiveFetch: FetchLike = config.fetchFn ?? fetch;
+ const url = `${config.baseURL}/models`;
- let response: Response;
- try {
- response = await effectiveFetch(url, {
- method: "GET",
- headers: {
- Authorization: `Bearer ${config.apiKey}`,
- },
- });
- } catch (err) {
- throw new Error(
- `listModels[${config.providerId}]: network error — ${err instanceof Error ? err.message : String(err)}`,
- );
- }
+ let response: Response;
+ try {
+ response = await effectiveFetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ },
+ });
+ } catch (err) {
+ throw new Error(
+ `listModels[${config.providerId}]: network error — ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
- if (!response.ok) {
- const text = await response.text().catch(() => "unknown");
- throw new Error(`listModels[${config.providerId}]: HTTP ${response.status} — ${text}`);
- }
+ if (!response.ok) {
+ const text = await response.text().catch(() => "unknown");
+ throw new Error(`listModels[${config.providerId}]: HTTP ${response.status} — ${text}`);
+ }
- const body = (await response.json()) as OpenAIModelListResponse;
- if (!Array.isArray(body.data)) {
- throw new Error(
- `listModels[${config.providerId}]: unexpected response shape — missing "data" array`,
- );
- }
+ const body = (await response.json()) as OpenAIModelListResponse;
+ if (!Array.isArray(body.data)) {
+ throw new Error(
+ `listModels[${config.providerId}]: unexpected response shape — missing "data" array`,
+ );
+ }
- return parseModelList(body.data);
+ return parseModelList(body.data);
}