| 1 | // Run: tsx src/__tests__/provider-vision-capability.test.ts |
| 2 | |
| 3 | import type { ProviderView } from "../lib/types"; |
| 4 | import { providerModelVisionCapability, providerVisionModelsForView } from "../lib/providerVisionCapability"; |
| 5 | import { createMockModelScopePreset } from "../lib/mockModelScopePreset"; |
| 6 | |
| 7 | let passed = 0; |
| 8 | let failed = 0; |
| 9 | |
| 10 | function ok(value: boolean, label: string) { |
| 11 | if (value) { |
| 12 | process.stdout.write(` PASS ${label}\n`); |
| 13 | passed += 1; |
| 14 | } else { |
| 15 | process.stdout.write(` FAIL ${label}\n`); |
| 16 | failed += 1; |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | function provider(overrides: Partial<ProviderView> = {}): ProviderView { |
| 21 | return { |
| 22 | name: "custom", |
| 23 | builtIn: false, |
| 24 | added: true, |
| 25 | kind: "openai", |
| 26 | baseUrl: "https://example.test/v1", |
| 27 | modelsUrl: "", |
| 28 | models: ["vision-model", "text-model", "opaque-model"], |
| 29 | visionModels: ["vision-model"], |
| 30 | visionModelsConfigured: true, |
| 31 | default: "vision-model", |
| 32 | apiKeyEnv: "CUSTOM_API_KEY", |
| 33 | keySet: true, |
| 34 | balanceUrl: "", |
| 35 | contextWindow: 0, |
| 36 | reasoningProtocol: "", |
| 37 | thinking: "", |
| 38 | supportedEfforts: [], |
| 39 | defaultEffort: "", |
| 40 | ...overrides, |
| 41 | }; |
| 42 | } |
| 43 | |
| 44 | const legacy = provider({ |
| 45 | modelOverrides: [{ |
| 46 | model: "vision-model", |
| 47 | reasoningProtocol: "", |
| 48 | supportedEfforts: [], |
| 49 | defaultEffort: "", |
| 50 | vision: false, |
| 51 | }], |
| 52 | }); |
| 53 | ok( |
| 54 | providerVisionModelsForView(legacy).length === 0, |
| 55 | "model override can disable a legacy vision list entry", |
| 56 | ); |
| 57 | |
| 58 | const metadata = provider({ |
| 59 | visionModels: [], |
| 60 | visionModelsConfigured: false, |
| 61 | modelCapabilities: [ |
| 62 | { model: "vision-model", inputModalities: ["text", "image"], state: "supported", source: "adapter" }, |
| 63 | { model: "text-model", inputModalities: ["text"], state: "unsupported", source: "adapter" }, |
| 64 | ], |
| 65 | modelOverrides: [], |
| 66 | }); |
| 67 | const adapterMetadataVision = providerVisionModelsForView(metadata); |
| 68 | ok(adapterMetadataVision.length === 1 && adapterMetadataVision[0] === "vision-model", "adapter metadata enables native vision without VisionModels"); |
| 69 | ok(providerModelVisionCapability(metadata, "text-model", adapterMetadataVision) === "unsupported", "adapter text-only metadata stays read-only"); |
| 70 | ok( |
| 71 | providerModelVisionCapability( |
| 72 | provider({ |
| 73 | visionModels: [], |
| 74 | visionModelsConfigured: false, |
| 75 | visionCapability: "unsupported", |
| 76 | modelCapabilities: [], |
| 77 | }), |
| 78 | "opaque-model", |
| 79 | [], |
| 80 | ) === "unsupported", |
| 81 | "legacy provider-level unsupported capability remains a read-only fallback", |
| 82 | ); |
| 83 | |
| 84 | const overrideMetadata = provider({ |
| 85 | visionModels: [], |
| 86 | visionModelsConfigured: false, |
| 87 | modelOverrides: [{ |
| 88 | model: "vision-model", |
| 89 | reasoningProtocol: "", |
| 90 | supportedEfforts: [], |
| 91 | defaultEffort: "", |
| 92 | vision: true, |
| 93 | }], |
| 94 | }); |
| 95 | const metadataVision = providerVisionModelsForView(overrideMetadata); |
| 96 | ok(metadataVision.length === 1 && metadataVision[0] === "vision-model", "model override enables native vision without VisionModels"); |
| 97 | ok(providerModelVisionCapability(overrideMetadata, "vision-model", metadataVision) === "supported", "supported model is marked read-only"); |
| 98 | ok(providerModelVisionCapability(overrideMetadata, "opaque-model", metadataVision) === "unknown", "unknown model stays conservative"); |
| 99 | |
| 100 | const modelScopePreset = createMockModelScopePreset( |
| 101 | (input) => ({ ...provider(), ...input, visionModels: [], visionModelsConfigured: false }), |
| 102 | (id, label, description, keyEnv, provider) => ({ id, label, description, keyEnv, provider, providers: [provider] }), |
| 103 | ); |
| 104 | const modelScopeVision = providerVisionModelsForView(modelScopePreset.provider); |
| 105 | ok(modelScopeVision.length === 3 && modelScopeVision.every((model) => model.startsWith("Qwen/")), "mock ModelScope preserves model-level vision metadata"); |
| 106 | |
| 107 | process.stdout.write(`provider vision capability: ${passed} passed, ${failed} failed\n`); |
| 108 | if (failed > 0) process.exitCode = 1; |
| 109 |