From 648bafb340113926e1c5942a2715558ec6f50f33 Mon Sep 17 00:00:00 2001 From: Tan Jia Huei Date: Tue, 3 Nov 2026 17:04:09 +0000 Subject: [PATCH 1/3] test: Preserve embedding provider and schema evidence --- .../recorded/cohere/batch-base64.yaml | 94 +++ .../recorded/cohere/batch-document.yaml | 665 ++++++++++++++++++ .../recorded/cohere/classification.yaml | 352 +++++++++ .../recorded/cohere/clustering.yaml | 352 +++++++++ .../recorded/cohere/invalid-model.yaml | 29 + .../embeddings/recorded/cohere/manifest.yaml | 28 + .../recorded/cohere/single-query.yaml | 351 +++++++++ .../recorded/openai/batch-base64.yaml | 42 ++ .../recorded/openai/batch-float.yaml | 61 ++ .../recorded/openai/default-dimensions.yaml | 177 +++++ .../recorded/openai/invalid-dimensions.yaml | 30 + .../embeddings/recorded/openai/manifest.yaml | 126 ++++ .../recorded/openai/missing-model.yaml | 31 + .../openai/single-omitted-encoding.yaml | 35 + .../recorded/openai/token-array-float.yaml | 46 ++ .../recorded/openai/token-batch-base64.yaml | 44 ++ .../recorded/voyageai/batch-base64.yaml | 27 + .../recorded/voyageai/batch-document.yaml | 26 + .../recorded/voyageai/invalid-model.yaml | 21 + .../recorded/voyageai/manifest.yaml | 25 + .../recorded/voyageai/single-default.yaml | 23 + .../recorded/voyageai/single-query.yaml | 24 + .../synthetic/infinity/batch-float.json | 48 ++ .../synthetic/infinity/dimensions.json | 47 ++ .../synthetic/infinity/manifest.json | 88 +++ .../synthetic/infinity/single-default.json | 32 + .../synthetic/infinity/token-batch-float.json | 53 ++ .../synthetic/infinity/tokens-base64.json | 35 + .../embeddings/synthetic/infinity/user.json | 37 + .../synthetic/vllm/batch-float.json | 51 ++ .../embeddings/synthetic/vllm/dimensions.json | 50 ++ .../embeddings/synthetic/vllm/manifest.json | 104 +++ .../synthetic/vllm/single-default.json | 35 + .../synthetic/vllm/token-batch-float.json | 56 ++ .../synthetic/vllm/token-options.json | 45 ++ .../synthetic/vllm/tokens-base64.json | 38 + .../embeddings/synthetic/vllm/user.json | 40 ++ 37 files changed, 3368 insertions(+) create mode 100644 tests/cassettes/embeddings/recorded/cohere/batch-base64.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/batch-document.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/classification.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/clustering.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/invalid-model.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/manifest.yaml create mode 100644 tests/cassettes/embeddings/recorded/cohere/single-query.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/batch-base64.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/batch-float.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/default-dimensions.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/invalid-dimensions.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/manifest.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/missing-model.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/single-omitted-encoding.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/token-array-float.yaml create mode 100644 tests/cassettes/embeddings/recorded/openai/token-batch-base64.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/batch-base64.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/batch-document.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/invalid-model.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/manifest.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/single-default.yaml create mode 100644 tests/cassettes/embeddings/recorded/voyageai/single-query.yaml create mode 100644 tests/cassettes/embeddings/synthetic/infinity/batch-float.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/dimensions.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/manifest.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/single-default.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json create mode 100644 tests/cassettes/embeddings/synthetic/infinity/user.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/batch-float.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/dimensions.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/manifest.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/single-default.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/token-options.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/user.json diff --git a/tests/cassettes/embeddings/recorded/cohere/batch-base64.yaml b/tests/cassettes/embeddings/recorded/cohere/batch-base64.yaml new file mode 100644 index 0000000..bf09551 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/batch-base64.yaml @@ -0,0 +1,94 @@ +timestamp: '2026-11-03T17:02:23.535600+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: embed-v4.0 + texts: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + input_type: search_document + truncate: NONE + embedding_types: + - base64 + output_dimension: 256 +response: + status_code: 200 + headers: + content-type: application/json + body: + id: 05544091-3720-4c95-a4b7-13e72d7b0a1b + texts: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + embeddings: + base64: + - HVnpvFqFEr31eYm9qQL2O9GgebxWBOO9ctirvNbjNDt5Hs+7p/+NvUBz1bj+gTi99LrlvKf/DT3o8Kq6W0eeuzvvvT1RgMs8LmbfvVqFEr5mT028WoUSO1fDBj2D5SG8qcGZPBjVUTyBZHI9wFKnuanBGb0al109QHPVvAbIW72TMAy+kzAMPc+dET0nILw9wFInvUW2kLwIiuc99XkJPrkMhDxhyzW9qcEZPXLYqzzAUic+9XmJvQlJC72N7dA98vjZPeRsk7uQbgA8fqJmuJbyF71M/LM9dZo3PKnBmT184Fo9WoWSPfA2Tjz5Pn09O+89vScgPD1IeBw9cBagvIanrT3o8Kq8+v2gvfS6ZT2dOLs8UYBLvakC9rr3O5U8XwmqvIsrRb1bRx69MCjrPL6QmzyD5aG8TPyzPHdcQ72LK0U91CEpPYanrb27zo87MChrveRsk7xF92w7f2EKvdGgeb2pwRm9j6/cPPL4Wb1F9+y8hqctvnzgWr0bVgG8XwkqvbAHPT0f2hi9PrFJvd0p2L3rsra75i4fvSuk0zpIeJw85GyTPAiK5713XEO9bZXwvPA2Tjy80fc7bZVwvi8lgz0dGI29AwbQu5i0ozxtlXA8O++9PJi0IzzwNk69gSMWPf6BOL4x5449cBYgvS8lgzzRoHk9iyvFPVtHnjusg6W9z50RvTSpGj1+ouY9eR5PvBtWgT1+ouY82aXAvbAHvTyTMAy6MChrPbhNYD1wFiA8qcEZvtFfHT6W8hc8j69cPbWL1DwuZl89GpddvfL42Txy2Cs9JF6wPR1Zab3UISm9LmbfvHdcQz0bVoE9qQL2PJbyFz0dWWk9u84PO7AHvbsNDn+8FRNGvTktsj0Y1dE98vjZvXzg2jxr0+S7LmZfvAlJC74JSYu90V8dvFfDBj3wNs691uM0PORskzyzyci8SjqovHkeT727zg+8gWRyPBtWAT05LbK9a9NkPayDpb1hy7W8LmbfvRtWgb35Pn29CUmLvCnixzsx5w4+Ds0iu+Vve72+kBu+bVSUPQbIW72uRTG8a9NkPQ7Nor2f+ka9kG6APXkeT7yBIxa9je1QPZp2rzu80Xc9K6TTvWKNQT0vJQM+T74/PPA2TryTMAy8kG6APTvvPb077729NKmaPfA2zjuf+sY9K6RTvRtWgT1UQte95GyTPc+dET4wKGu9QvSEvdbjNL1wFqA9gWRyPEh4HL67zo89orxSPKnBmT3dKVg8uQyEvY+vXLwf2pg8bZVwvfc7lb1FtpA9HVlpvaK80rxPvr88CIrnPHLYq7wY1VG8AwZQPUL0hD21i9S9mLQjPeDrY7184Fq6H9qYPO10wjz3O5U80aB5vQ== + - Du0xvStDw70O7TG9ABQRPeewCj1q4nC9QgJ6vIeAIT24Ri494xkwvuMZMD0r5xK9ABSRvfrY5ruaqH2952hrvJW1cj0FB5w83oLVvD9XDr4/Vw69lbVyvHQQJj2Rwmc7ffY7PWCgKr3x8lA9zwXlPDrAsz2aqH29h4ChvZX9kb0iuV2+QgL6PMu2qT28lek9qcm9Om95y7uH3NE9b9X7PdScPzxlkzW9Pg/vPILpxj1WFkU+XAnQvIKNljtlN4U9vN2IPCb0hzxveUs9lf2RvOdoa71WFsU7SJlUPROEjD1Sxwk9h4AhPZ4/2DykegI9+tjmvBxqIr2pyb09VroUPH2aiz23ol48QgJ6PA6RATwcaiI952jrvMUfT737IAY9i8/cvOewir01Kdm89okrPVLHCTxgoCq+ZTeFPaR6gr1Sx4k58ZagPfsgBr3idWC8Pg/vvD9XDj1Sxwk9b9X7vJ/jp7z25du97f/FvBM87bzPBWU9h9zRva1gGLx4A7G8IV0tPHingL1q4nC9IV0tvfGWIL1HPaQ7vN0IvHgDMb28OTm9YKAqPe3/Rb6Rwue7PrM+vSb0h72HgKE97KMVvk2M3z1vecu9uEauPJqo/bqaTM08BQccvgRjTL283Yi9kQoHvVa6FL7UnL89h4AhvcZ7fz1goKo9lf2RPVIjOr3naOu9h4AhPH2aiz1Sxwk+OmQDvbw5OTp9mgs9Fy/4vTDaHTxlNwW98ZYgPQUHnDz25Vu71Jy/vUiZ1D10EKa8UiO6PDUpWT19mgs9JqxovWmGQD0m9Ie8Q0qZuz9XjrzUQI+9auLwvYvPXDt4AzE9b3lLPSasaL0m9Ic8UscJvdkzmjxWFkW8JvQHvbw5uT3t/8U9go0Wu+xb9ruaqP28pDLjvYtzLL0rQ0O8pHoCvd6CVT37IAa9TYxfPIvP3D2yr9O9CVZXvWD82r2H3FG8dBCmukICer3naOu8mkxNPCK5XTveJiU8bx2bvIfc0bzx8lC9YKAqvLzdCL0w2h09Pg9vu4KNFr3nsAq+t6JePVYWRT3Ge389mqh9u5qofTxq4vC9DUniO94mJTyH3NE8PrM+vaQyY73nDLu6BGNMvU0wrz325ds9LzZOPfHy0LzUQA+9b9V7PHingL2yr1O9+thmPbzdiDuobQ0+i3MsvcCIdDzeglW9Fy/4vE0wLz14pwC9kcJnvQRjTL1goKo9aiqQu88F5b2VtfI9wIh0Pedoaz3so5U9lbVyva1gGDxv1fu8sq9TvcHQk723ot68ZO/lvNiPSr3nsAo9i3Osu28dGz2CjRY9yxJaPZVZwj24Rq69vN0IPdkzmr0FBxw62TMavbyV6TvxliA9xcMePQ== + meta: + api_version: + version: '2' + billed_units: + input_tokens: 17 + image_tokens: 0 + response_type: embeddings_by_type + body_bytes: !!binary | + eyJpZCI6IjA1NTQ0MDkxLTM3MjAtNGM5NS1hNGI3LTEzZTcyZDdiMGExYiIsInRleHRzIjpbIlBl + bmd1aW5zIGxpdmUgaW4gdGhlIFNvdXRoZXJuIEhlbWlzcGhlcmUuIiwiUG9sYXIgYmVhcnMgbGl2 + ZSBpbiB0aGUgQXJjdGljLiJdLCJlbWJlZGRpbmdzIjp7ImJhc2U2NCI6WyJIVm5wdkZxRkVyMzFl + WW05cVFMMk85R2dlYnhXQk9POWN0aXJ2TmJqTkR0NUhzKzdwLytOdlVCejFiaitnVGk5OUxybHZL + Zi9EVDNvOEtxNlcwZWV1enZ2dlQxUmdNczhMbWJmdlZxRkVyNW1UMDI4V29VU08xZkRCajJENVNH + OHFjR1pQQmpWVVR5QlpISTl3RktudWFuQkdiMGFsMTA5UUhQVnZBYklXNzJUTUF5K2t6QU1QYytk + RVQwbklMdzl3RkludlVXMmtMd0lpdWM5OVhrSlBya01oRHhoeXpXOXFjRVpQWExZcXp6QVVpYys5 + WG1KdlFsSkM3Mk43ZEE5OHZqWlBlUnNrN3VRYmdBOGZxSm11SmJ5RjcxTS9MTTlkWm8zUEtuQm1U + MTg0Rm85V29XU1BmQTJUano1UG4wOU8rODl2U2NnUEQxSWVCdzljQmFndklhbnJUM284S3E4K3Yy + Z3ZmUzZaVDJkT0xzOFVZQkx2YWtDOXJyM081VThYd21xdklzclJiMWJSeDY5TUNqclBMNlFtenlE + NWFHOFRQeXpQSGRjUTcyTEswVTkxQ0VwUFlhbnJiMjd6bzg3TUNocnZlUnNrN3hGOTJ3N2YyRUt2 + ZEdnZWIycHdSbTlqNi9jUFBMNFdiMUY5K3k4aHFjdHZuemdXcjBiVmdHOFh3a3F2YkFIUFQwZjJo + aTlQckZKdmQwcDJMM3JzcmE3NWk0ZnZTdWswenBJZUp3ODVHeVRQQWlLNTcxM1hFTzliWlh3dlBB + MlRqeTgwZmM3YlpWd3ZpOGxnejBkR0kyOUF3YlF1NWkwb3p4dGxYQThPKys5UEppMEl6endOazY5 + Z1NNV1BmNkJPTDR4NTQ0OWNCWWd2UzhsZ3p6Um9IazlpeXZGUFZ0SG5qdXNnNlc5ejUwUnZUU3BH + ajErb3VZOWVSNVB2QnRXZ1QxK291WTgyYVhBdmJBSHZUeVRNQXk2TUNoclBiaE5ZRDF3RmlBOHFj + RVp2dEZmSFQ2VzhoYzhqNjljUGJXTDFEd3VabDg5R3BkZHZmTDQyVHh5MkNzOUpGNndQUjFaYWIz + VUlTbTlMbWJmdkhkY1F6MGJWb0U5cVFMMlBKYnlGejBkV1drOXU4NFBPN0FIdmJzTkRuKzhGUk5H + dlRrdHNqMFkxZEU5OHZqWnZYemcyanhyMCtTN0xtWmZ2QWxKQzc0SlNZdTkwVjhkdkZmREJqM3dO + czY5MXVNMFBPUnNrenl6eWNpOFNqcW92SGtlVDcyN3pnKzhnV1J5UEJ0V0FUMDVMYks5YTlOa1Bh + eURwYjFoeTdXOExtYmZ2UnRXZ2IzNVBuMjlDVW1MdkNuaXh6c3g1dzQrRHMwaXUrVnZlNzIra0J1 + K2JWU1VQUWJJVzcydVJURzhhOU5rUFE3Tm9yMmYra2E5a0c2QVBYa2VUN3lCSXhhOWplMVFQWnAy + cnp1ODBYYzlLNlRUdldLTlFUMHZKUU0rVDc0L1BQQTJUcnlUTUF5OGtHNkFQVHZ2UGIwNzc3MjlO + S21hUGZBMnpqdWYrc1k5SzZSVHZSdFdnVDFVUXRlOTVHeVRQYytkRVQ0d0tHdTlRdlNFdmRiak5M + MXdGcUE5Z1dSeVBFaDRITDY3em84OW9yeFNQS25CbVQzZEtWZzh1UXlFdlkrdlhMd2YycGc4YlpW + d3ZmYzdsYjFGdHBBOUhWbHB2YUs4MHJ4UHZyODhDSXJuUEhMWXE3d1kxVkc4QXdaUVBVTDBoRDIx + aTlTOW1MUWpQZURyWTcxODRGcTZIOXFZUE8xMHdqejNPNVU4MGFCNXZRPT0iLCJEdTB4dlN0RHc3 + ME83VEc5QUJRUlBlZXdDajFxNG5DOVFnSjZ2SWVBSVQyNFJpNDk0eGt3dnVNWk1EMHI1eEs5QUJT + UnZmclk1cnVhcUgyOTUyaHJ2SlcxY2owRkI1dzgzb0xWdkQ5WERyNC9WdzY5bGJWeXZIUVFKajJS + d21jN2ZmWTdQV0NnS3IzeDhsQTl6d1hsUERyQXN6MmFxSDI5aDRDaHZaWDlrYjBpdVYyK1FnTDZQ + TXUycVQyOGxlazlxY205T205NXk3dUgzTkU5YjlYN1BkU2NQenhsa3pXOVBnL3ZQSUxweGoxV0Zr + VStYQW5RdklLTmxqdGxONFU5dk4ySVBDYjBoenh2ZVVzOWxmMlJ2T2RvYTcxV0ZzVTdTSmxVUFJP + RWpEMVN4d2s5aDRBaFBaNC8yRHlrZWdJOSt0am12QnhxSXIycHliMDlWcm9VUEgyYWl6MjNvbDQ4 + UWdKNlBBNlJBVHdjYWlJOTUyanJ2TVVmVDczN0lBWTlpOC9jdk9ld2lyMDFLZG04OW9rclBWTEhD + VHhnb0NxK1pUZUZQYVI2Z3IxU3g0azU4WmFnUGZzZ0JyM2lkV0M4UGcvdnZEOVhEajFTeHdrOWI5 + WDd2Si9qcDd6MjVkdTk3Zi9GdkJNODdielBCV1U5aDl6UnZhMWdHTHg0QTdHOElWMHRQSGluZ0wx + cTRuQzlJVjB0dmZHV0lMMUhQYVE3dk4wSXZIZ0RNYjI4T1RtOVlLQXFQZTMvUmI2Und1ZTdQck0r + dlNiMGg3MkhnS0U5N0tNVnZrMk0zejF2ZWN1OXVFYXVQSnFvL2JxYVRNMDhCUWNjdmdSalRMMjgz + WWk5a1FvSHZWYTZGTDdVbkw4OWg0QWh2Y1o3ZnoxZ29LbzlsZjJSUFZJak9yM25hT3U5aDRBaFBI + MmFpejFTeHdrK09tUUR2Ync1T1RwOW1nczlGeS80dlREYUhUeGxOd1c5OFpZZ1BRVUhuRHoyNVZ1 + NzFKeS92VWlaMUQxMEVLYThVaU82UERVcFdUMTltZ3M5SnF4b3ZXbUdRRDBtOUllOFEwcVp1ejlY + anJ6VVFJKzlhdUx3dll2UFhEdDRBekU5YjNsTFBTYXNhTDBtOUljOFVzY0p2ZGt6bWp4V0ZrVzhK + dlFIdmJ3NXVUM3QvOFU5Z28wV3UreGI5cnVhcVAyOHBETGp2WXR6TEwwclEwTzhwSG9DdmQ2Q1ZU + MzdJQWE5VFl4ZlBJdlAzRDJ5cjlPOUNWWlh2V0Q4MnIySDNGRzhkQkNtdWtJQ2VyM25hT3U4bWt4 + TlBDSzVYVHZlSmlVOGJ4MmJ2SWZjMGJ6eDhsQzlZS0Fxdkx6ZENMMHcyaDA5UGc5dnU0S05GcjNu + c0FxK3Q2SmVQVllXUlQzR2UzODltcWg5dTVxb2ZUeHE0dkM5RFVuaU85NG1KVHlIM05FOFByTSt2 + YVF5WTczbkRMdTZCR05NdlUwd3J6MzI1ZHM5THpaT1BmSHkwTHpVUUErOWI5VjdQSGluZ0wyeXIx + TzkrdGhtUGJ6ZGlEdW9iUTAraTNNc3ZjQ0lkRHplZ2xXOUZ5LzR2RTB3THoxNHB3QzlrY0pudlFS + alRMMWdvS285YWlxUXU4OEY1YjJWdGZJOXdJaDBQZWRvYXozc281VTlsYlZ5dmExZ0dEeHYxZnU4 + c3E5VHZjSFFrNzIzb3Q2OFpPL2x2TmlQU3IzbnNBbzlpM09zdTI4ZEd6MkNqUlk5eXhKYVBaVlp3 + ajI0UnE2OXZOMElQZGt6bXIwRkJ4dzYyVE1hdmJ5VjZUdnhsaUE5eGNNZVBRPT0iXX0sIm1ldGEi + OnsiYXBpX3ZlcnNpb24iOnsidmVyc2lvbiI6IjIifSwiYmlsbGVkX3VuaXRzIjp7ImlucHV0X3Rv + a2VucyI6MTcsImltYWdlX3Rva2VucyI6MH19LCJyZXNwb25zZV90eXBlIjoiZW1iZWRkaW5nc19i + eV90eXBlIn0= diff --git a/tests/cassettes/embeddings/recorded/cohere/batch-document.yaml b/tests/cassettes/embeddings/recorded/cohere/batch-document.yaml new file mode 100644 index 0000000..cf7e848 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/batch-document.yaml @@ -0,0 +1,665 @@ +timestamp: '2026-11-03T17:02:23.163956+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: embed-v4.0 + texts: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + input_type: search_document + truncate: NONE + embedding_types: + - float + output_dimension: 256 +response: + status_code: 200 + headers: + content-type: application/json + body: + id: b8c36ac4-e08f-4c33-ac9a-bb68dd1dced3 + texts: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + embeddings: + float: + - - -0.03070113 + - -0.035560302 + - -0.06670318 + - 0.0072887572 + - -0.0150192585 + - -0.10999397 + - -0.02297063 + - 0.0016496335 + - -0.0048315628 + - -0.07067886 + - 0.00042793842 + - -0.04549952 + - -0.028492417 + - 0.033130717 + - -0.0029541554 + - -0.004500256 + - 0.09497472 + - 0.025621088 + - -0.1091105 + - -0.14489166 + - -0.013914901 + - 0.00039860394 + - 0.034455944 + - -0.012037494 + - 0.017007101 + - 0.017007101 + - 0.057868317 + - -0.00013718188 + - -0.03644379 + - 0.053671762 + - -0.02451673 + - -0.05389263 + - -0.13605681 + - 0.034455944 + - 0.03644379 + - 0.09144078 + - -0.040861215 + - -0.018221894 + - 0.112644434 + - 0.13428983 + - 0.016786229 + - -0.042186446 + - 0.038652502 + - 0.021424528 + - 0.1607944 + - -0.064052716 + - -0.032026358 + - 0.10160086 + - 0.10425132 + - -0.005908311 + - 0.008669204 + - -0.0015530023 + - -0.036885533 + - 0.08746509 + - 0.012479236 + - 0.07377107 + - 0.05345089 + - 0.07023712 + - 0.012479236 + - 0.06361098 + - -0.044836905 + - 0.047266487 + - 0.03533943 + - -0.017669715 + - 0.08260592 + - -0.021314096 + - -0.08083895 + - 0.057868317 + - 0.023412373 + - -0.05035869 + - -0.004445038 + - 0.018884508 + - -0.022860195 + - -0.048149977 + - -0.041082088 + - 0.029817645 + - 0.019767994 + - -0.020541044 + - 0.022860195 + - -0.046824746 + - 0.04549952 + - 0.039756857 + - -0.082164176 + - 0.005715049 + - -0.057868317 + - -0.016675794 + - 0.00593592 + - -0.0324681 + - -0.05963529 + - -0.038873374 + - 0.025841957 + - -0.052788273 + - -0.027498493 + - -0.16962928 + - -0.051904786 + - -0.006764188 + - -0.043511674 + - 0.045057774 + - -0.037106402 + - -0.053009145 + - -0.10513481 + - -0.0054665683 + - -0.03644379 + - 0.001456371 + - 0.019215815 + - 0.014467079 + - -0.11308618 + - -0.048812587 + - -0.03070113 + - 0.011927058 + - 0.0036995967 + - -0.23324023 + - 0.064052716 + - -0.07067886 + - -0.0065157074 + - 0.017227972 + - 0.014908822 + - 0.024185423 + - 0.009387036 + - -0.053009145 + - 0.035560302 + - -0.18199807 + - 0.06670318 + - -0.040419474 + - 0.016123615 + - 0.06361098 + - 0.09364949 + - 0.007840936 + - -0.0803972 + - -0.03489769 + - 0.035560302 + - 0.11308618 + - -0.009607907 + - 0.06140226 + - 0.027498493 + - -0.09144078 + - 0.025510652 + - -0.0022363234 + - 0.054996986 + - 0.054996986 + - 0.008558768 + - -0.15372652 + - 0.15107606 + - 0.0096631255 + - 0.05035869 + - 0.02451673 + - 0.055880476 + - -0.053009145 + - 0.028050672 + - 0.040640343 + - 0.08746509 + - -0.056984834 + - -0.041082088 + - -0.024958473 + - 0.048149977 + - 0.0649362 + - 0.028713286 + - 0.03533943 + - 0.055659603 + - 0.0026228484 + - -0.005024825 + - -0.014246209 + - -0.048370846 + - 0.08613986 + - 0.10248435 + - -0.106460035 + - 0.025731524 + - -0.007951372 + - -0.012976197 + - -0.13870727 + - -0.06979538 + - -0.011264443 + - 0.033572458 + - -0.09983389 + - 0.008890076 + - 0.019767994 + - -0.023081064 + - -0.020209737 + - -0.050137818 + - -0.012865762 + - 0.017007101 + - 0.03224723 + - -0.08746509 + - 0.057868317 + - -0.078188494 + - -0.018663635 + - -0.10778526 + - -0.061844002 + - -0.05963529 + - -0.017448844 + - 0.0077305003 + - 0.14135772 + - -0.0026090438 + - -0.06140226 + - -0.15195955 + - 0.07553803 + - -0.054334376 + - -0.012534454 + - 0.054113504 + - -0.07774675 + - -0.04925433 + - 0.06449446 + - -0.011761405 + - -0.039094247 + - 0.051242176 + - 0.003975686 + - 0.061844002 + - -0.10336783 + - 0.049916945 + - 0.1298724 + - 0.011540532 + - -0.010215304 + - -0.010270522 + - 0.064052716 + - -0.04748736 + - -0.09144078 + - 0.07332932 + - 0.008393114 + - 0.09674169 + - -0.05080043 + - 0.06140226 + - -0.1091105 + - 0.07244583 + - 0.14047424 + - -0.057868317 + - -0.0649362 + - -0.045278646 + - 0.0803972 + - 0.014467079 + - -0.15284304 + - 0.07288758 + - 0.014246209 + - 0.07642152 + - 0.010546612 + - -0.06537794 + - -0.013031415 + - 0.01965756 + - -0.059193548 + - -0.07377107 + - 0.07332932 + - -0.056984834 + - -0.023633244 + - 0.0247376 + - 0.028271545 + - -0.021424528 + - -0.012865762 + - 0.05389263 + - 0.06581969 + - -0.10425132 + - 0.041082088 + - -0.05389263 + - -0.0034787252 + - 0.018001024 + - 0.02120366 + - 0.01965756 + - -0.05963529 + - - -0.044520803 + - -0.093938895 + - -0.042962577 + - 0.034058414 + - 0.0333906 + - -0.05698663 + - -0.015582281 + - 0.03717487 + - 0.042962577 + - -0.17006947 + - 0.042517368 + - -0.03450362 + - -0.0721237 + - -0.0062885634 + - -0.062329125 + - -0.016361395 + - 0.059657875 + - 0.02003436 + - -0.0277142 + - -0.13890491 + - -0.03472623 + - -0.016806604 + - 0.039400913 + - 0.0031999326 + - 0.045633823 + - -0.043407783 + - 0.050085902 + - 0.027380293 + - 0.085925154 + - -0.06277433 + - -0.0774662 + - -0.06945246 + - -0.21637112 + - 0.031387165 + - 0.080582656 + - 0.11397326 + - 0.0012730167 + - -0.005425973 + - 0.10150743 + - 0.12465825 + - 0.009126765 + - -0.044743404 + - 0.030051542 + - 0.09705535 + - 0.19054905 + - -0.025822066 + - 0.0040068724 + - 0.06589079 + - 0.016361395 + - 0.01714051 + - 0.047192052 + - -0.015582281 + - -0.057877045 + - 0.0077911406 + - 0.052979756 + - 0.06900724 + - 0.034058414 + - 0.0387331 + - 0.02604467 + - 0.032277584 + - -0.028715918 + - -0.040736534 + - 0.09171285 + - 0.010406738 + - 0.06900724 + - 0.014691866 + - 0.013467543 + - 0.0071789795 + - 0.038287893 + - -0.029161125 + - -0.050753716 + - 0.03272279 + - -0.026823783 + - -0.06500037 + - -0.026155973 + - 0.041849554 + - 0.008570255 + - -0.16739821 + - 0.06277433 + - -0.06366475 + - 0.00043129525 + - 0.078356616 + - -0.034058414 + - -0.012577127 + - -0.0277142 + - 0.033168 + - 0.03272279 + - -0.029161125 + - -0.020702174 + - -0.10684993 + - -0.024041234 + - -0.026378576 + - 0.05698663 + - -0.100171804 + - -0.008069396 + - -0.022928212 + - 0.009571973 + - -0.06410996 + - -0.059657875 + - -0.041626953 + - -0.03762008 + - 0.005425973 + - -0.0066781207 + - -0.04318518 + - -0.044966012 + - 0.043630388 + - -0.19500113 + - -0.009071114 + - -0.045856424 + - -0.06811683 + - 0.08147307 + - -0.14691865 + - 0.10907597 + - -0.10373347 + - 0.02059087 + - -0.0012938859 + - 0.024486441 + - -0.15137073 + - -0.050531108 + - -0.06722642 + - -0.03361321 + - -0.14691865 + - 0.09260327 + - -0.039623514 + - 0.06143871 + - 0.08147307 + - 0.07078808 + - -0.046079032 + - -0.11397326 + - 0.01001718 + - 0.07034287 + - 0.1335624 + - -0.031832375 + - 0.0016417046 + - 0.03494883 + - -0.12020617 + - 0.0074572344 + - -0.0333906 + - 0.03984612 + - 0.02059087 + - -0.0033947113 + - -0.095719725 + - 0.10506909 + - -0.019143946 + - 0.024597744 + - 0.051866736 + - 0.03428102 + - -0.05832225 + - 0.04875028 + - -0.015359677 + - -0.0062885634 + - -0.017808322 + - -0.06811683 + - -0.11931575 + - 0.0040903487 + - 0.044298198 + - 0.050531108 + - -0.057431836 + - 0.015804885 + - -0.035171434 + - 0.017808322 + - -0.014914469 + - -0.03450362 + - 0.091267645 + - 0.097945765 + - 0.000426078 + - -0.008681557 + - -0.030719355 + - -0.10996638 + - -0.04118174 + - -0.011909315 + - -0.030941958 + - 0.05164413 + - -0.032277584 + - 0.012243221 + - 0.10952117 + - -0.10284305 + - -0.052534547 + - -0.10417868 + - -0.011408456 + - 0.0013495368 + - -0.06277433 + - -0.02827071 + - 0.011742362 + - 0.0039233956 + - 0.009738925 + - -0.020256966 + - -0.025710763 + - -0.050753716 + - -0.010295435 + - -0.03383581 + - 0.04118174 + - -0.0043964293 + - -0.036729664 + - -0.1335624 + - 0.05609621 + - 0.047859862 + - 0.061883915 + - -0.0043129525 + - 0.01825353 + - -0.11931575 + - 0.0047859866 + - 0.007846791 + - 0.024820348 + - -0.045856424 + - -0.05275715 + - -0.0029495033 + - -0.050531108 + - 0.085034736 + - 0.10774034 + - 0.051866736 + - -0.027380293 + - -0.035171434 + - 0.015693583 + - -0.06321954 + - -0.052979756 + - 0.057431836 + - 0.0043964293 + - 0.13801448 + - -0.042739972 + - 0.012577127 + - -0.05275715 + - -0.029828938 + - 0.043407783 + - -0.03049675 + - -0.057431836 + - -0.050085902 + - 0.086815566 + - -0.0038120938 + - -0.11219242 + - 0.11931575 + - 0.05921267 + - 0.055205792 + - 0.07123329 + - -0.05832225 + - 0.010907597 + - -0.032945395 + - -0.051198922 + - -0.07034287 + - -0.026823783 + - -0.025599461 + - -0.048527677 + - 0.03472623 + - -0.00500859 + - 0.03806529 + - 0.03717487 + - 0.05320236 + - 0.094384104 + - -0.085925154 + - 0.03472623 + - -0.07524016 + - -0.00075128855 + - -0.03695227 + - 0.006483342 + - 0.040291328 + - 0.037397474 + meta: + api_version: + version: '2' + billed_units: + input_tokens: 17 + image_tokens: 0 + response_type: embeddings_by_type + body_bytes: !!binary | + eyJpZCI6ImI4YzM2YWM0LWUwOGYtNGMzMy1hYzlhLWJiNjhkZDFkY2VkMyIsInRleHRzIjpbIlBl + bmd1aW5zIGxpdmUgaW4gdGhlIFNvdXRoZXJuIEhlbWlzcGhlcmUuIiwiUG9sYXIgYmVhcnMgbGl2 + ZSBpbiB0aGUgQXJjdGljLiJdLCJlbWJlZGRpbmdzIjp7ImZsb2F0IjpbWy0wLjAzMDcwMTEzLC0w + LjAzNTU2MDMwMiwtMC4wNjY3MDMxOCwwLjAwNzI4ODc1NzIsLTAuMDE1MDE5MjU4NSwtMC4xMDk5 + OTM5NywtMC4wMjI5NzA2MywwLjAwMTY0OTYzMzUsLTAuMDA0ODMxNTYyOCwtMC4wNzA2Nzg4Niww + LjAwMDQyNzkzODQyLC0wLjA0NTQ5OTUyLC0wLjAyODQ5MjQxNywwLjAzMzEzMDcxNywtMC4wMDI5 + NTQxNTU0LC0wLjAwNDUwMDI1NiwwLjA5NDk3NDcyLDAuMDI1NjIxMDg4LC0wLjEwOTExMDUsLTAu + MTQ0ODkxNjYsLTAuMDEzOTE0OTAxLDAuMDAwMzk4NjAzOTQsMC4wMzQ0NTU5NDQsLTAuMDEyMDM3 + NDk0LDAuMDE3MDA3MTAxLDAuMDE3MDA3MTAxLDAuMDU3ODY4MzE3LC0wLjAwMDEzNzE4MTg4LC0w + LjAzNjQ0Mzc5LDAuMDUzNjcxNzYyLC0wLjAyNDUxNjczLC0wLjA1Mzg5MjYzLC0wLjEzNjA1Njgx + LDAuMDM0NDU1OTQ0LDAuMDM2NDQzNzksMC4wOTE0NDA3OCwtMC4wNDA4NjEyMTUsLTAuMDE4MjIx + ODk0LDAuMTEyNjQ0NDM0LDAuMTM0Mjg5ODMsMC4wMTY3ODYyMjksLTAuMDQyMTg2NDQ2LDAuMDM4 + NjUyNTAyLDAuMDIxNDI0NTI4LDAuMTYwNzk0NCwtMC4wNjQwNTI3MTYsLTAuMDMyMDI2MzU4LDAu + MTAxNjAwODYsMC4xMDQyNTEzMiwtMC4wMDU5MDgzMTEsMC4wMDg2NjkyMDQsLTAuMDAxNTUzMDAy + MywtMC4wMzY4ODU1MzMsMC4wODc0NjUwOSwwLjAxMjQ3OTIzNiwwLjA3Mzc3MTA3LDAuMDUzNDUw + ODksMC4wNzAyMzcxMiwwLjAxMjQ3OTIzNiwwLjA2MzYxMDk4LC0wLjA0NDgzNjkwNSwwLjA0NzI2 + NjQ4NywwLjAzNTMzOTQzLC0wLjAxNzY2OTcxNSwwLjA4MjYwNTkyLC0wLjAyMTMxNDA5NiwtMC4w + ODA4Mzg5NSwwLjA1Nzg2ODMxNywwLjAyMzQxMjM3MywtMC4wNTAzNTg2OSwtMC4wMDQ0NDUwMzgs + MC4wMTg4ODQ1MDgsLTAuMDIyODYwMTk1LC0wLjA0ODE0OTk3NywtMC4wNDEwODIwODgsMC4wMjk4 + MTc2NDUsMC4wMTk3Njc5OTQsLTAuMDIwNTQxMDQ0LDAuMDIyODYwMTk1LC0wLjA0NjgyNDc0Niww + LjA0NTQ5OTUyLDAuMDM5NzU2ODU3LC0wLjA4MjE2NDE3NiwwLjAwNTcxNTA0OSwtMC4wNTc4Njgz + MTcsLTAuMDE2Njc1Nzk0LDAuMDA1OTM1OTIsLTAuMDMyNDY4MSwtMC4wNTk2MzUyOSwtMC4wMzg4 + NzMzNzQsMC4wMjU4NDE5NTcsLTAuMDUyNzg4MjczLC0wLjAyNzQ5ODQ5MywtMC4xNjk2MjkyOCwt + MC4wNTE5MDQ3ODYsLTAuMDA2NzY0MTg4LC0wLjA0MzUxMTY3NCwwLjA0NTA1Nzc3NCwtMC4wMzcx + MDY0MDIsLTAuMDUzMDA5MTQ1LC0wLjEwNTEzNDgxLC0wLjAwNTQ2NjU2ODMsLTAuMDM2NDQzNzks + MC4wMDE0NTYzNzEsMC4wMTkyMTU4MTUsMC4wMTQ0NjcwNzksLTAuMTEzMDg2MTgsLTAuMDQ4ODEy + NTg3LC0wLjAzMDcwMTEzLDAuMDExOTI3MDU4LDAuMDAzNjk5NTk2NywtMC4yMzMyNDAyMywwLjA2 + NDA1MjcxNiwtMC4wNzA2Nzg4NiwtMC4wMDY1MTU3MDc0LDAuMDE3MjI3OTcyLDAuMDE0OTA4ODIy + LDAuMDI0MTg1NDIzLDAuMDA5Mzg3MDM2LC0wLjA1MzAwOTE0NSwwLjAzNTU2MDMwMiwtMC4xODE5 + OTgwNywwLjA2NjcwMzE4LC0wLjA0MDQxOTQ3NCwwLjAxNjEyMzYxNSwwLjA2MzYxMDk4LDAuMDkz + NjQ5NDksMC4wMDc4NDA5MzYsLTAuMDgwMzk3MiwtMC4wMzQ4OTc2OSwwLjAzNTU2MDMwMiwwLjEx + MzA4NjE4LC0wLjAwOTYwNzkwNywwLjA2MTQwMjI2LDAuMDI3NDk4NDkzLC0wLjA5MTQ0MDc4LDAu + MDI1NTEwNjUyLC0wLjAwMjIzNjMyMzQsMC4wNTQ5OTY5ODYsMC4wNTQ5OTY5ODYsMC4wMDg1NTg3 + NjgsLTAuMTUzNzI2NTIsMC4xNTEwNzYwNiwwLjAwOTY2MzEyNTUsMC4wNTAzNTg2OSwwLjAyNDUx + NjczLDAuMDU1ODgwNDc2LC0wLjA1MzAwOTE0NSwwLjAyODA1MDY3MiwwLjA0MDY0MDM0MywwLjA4 + NzQ2NTA5LC0wLjA1Njk4NDgzNCwtMC4wNDEwODIwODgsLTAuMDI0OTU4NDczLDAuMDQ4MTQ5OTc3 + LDAuMDY0OTM2MiwwLjAyODcxMzI4NiwwLjAzNTMzOTQzLDAuMDU1NjU5NjAzLDAuMDAyNjIyODQ4 + NCwtMC4wMDUwMjQ4MjUsLTAuMDE0MjQ2MjA5LC0wLjA0ODM3MDg0NiwwLjA4NjEzOTg2LDAuMTAy + NDg0MzUsLTAuMTA2NDYwMDM1LDAuMDI1NzMxNTI0LC0wLjAwNzk1MTM3MiwtMC4wMTI5NzYxOTcs + LTAuMTM4NzA3MjcsLTAuMDY5Nzk1MzgsLTAuMDExMjY0NDQzLDAuMDMzNTcyNDU4LC0wLjA5OTgz + Mzg5LDAuMDA4ODkwMDc2LDAuMDE5NzY3OTk0LC0wLjAyMzA4MTA2NCwtMC4wMjAyMDk3MzcsLTAu + MDUwMTM3ODE4LC0wLjAxMjg2NTc2MiwwLjAxNzAwNzEwMSwwLjAzMjI0NzIzLC0wLjA4NzQ2NTA5 + LDAuMDU3ODY4MzE3LC0wLjA3ODE4ODQ5NCwtMC4wMTg2NjM2MzUsLTAuMTA3Nzg1MjYsLTAuMDYx + ODQ0MDAyLC0wLjA1OTYzNTI5LC0wLjAxNzQ0ODg0NCwwLjAwNzczMDUwMDMsMC4xNDEzNTc3Miwt + MC4wMDI2MDkwNDM4LC0wLjA2MTQwMjI2LC0wLjE1MTk1OTU1LDAuMDc1NTM4MDMsLTAuMDU0MzM0 + Mzc2LC0wLjAxMjUzNDQ1NCwwLjA1NDExMzUwNCwtMC4wNzc3NDY3NSwtMC4wNDkyNTQzMywwLjA2 + NDQ5NDQ2LC0wLjAxMTc2MTQwNSwtMC4wMzkwOTQyNDcsMC4wNTEyNDIxNzYsMC4wMDM5NzU2ODYs + MC4wNjE4NDQwMDIsLTAuMTAzMzY3ODMsMC4wNDk5MTY5NDUsMC4xMjk4NzI0LDAuMDExNTQwNTMy + LC0wLjAxMDIxNTMwNCwtMC4wMTAyNzA1MjIsMC4wNjQwNTI3MTYsLTAuMDQ3NDg3MzYsLTAuMDkx + NDQwNzgsMC4wNzMzMjkzMiwwLjAwODM5MzExNCwwLjA5Njc0MTY5LC0wLjA1MDgwMDQzLDAuMDYx + NDAyMjYsLTAuMTA5MTEwNSwwLjA3MjQ0NTgzLDAuMTQwNDc0MjQsLTAuMDU3ODY4MzE3LC0wLjA2 + NDkzNjIsLTAuMDQ1Mjc4NjQ2LDAuMDgwMzk3MiwwLjAxNDQ2NzA3OSwtMC4xNTI4NDMwNCwwLjA3 + Mjg4NzU4LDAuMDE0MjQ2MjA5LDAuMDc2NDIxNTIsMC4wMTA1NDY2MTIsLTAuMDY1Mzc3OTQsLTAu + MDEzMDMxNDE1LDAuMDE5NjU3NTYsLTAuMDU5MTkzNTQ4LC0wLjA3Mzc3MTA3LDAuMDczMzI5MzIs + LTAuMDU2OTg0ODM0LC0wLjAyMzYzMzI0NCwwLjAyNDczNzYsMC4wMjgyNzE1NDUsLTAuMDIxNDI0 + NTI4LC0wLjAxMjg2NTc2MiwwLjA1Mzg5MjYzLDAuMDY1ODE5NjksLTAuMTA0MjUxMzIsMC4wNDEw + ODIwODgsLTAuMDUzODkyNjMsLTAuMDAzNDc4NzI1MiwwLjAxODAwMTAyNCwwLjAyMTIwMzY2LDAu + MDE5NjU3NTYsLTAuMDU5NjM1MjldLFstMC4wNDQ1MjA4MDMsLTAuMDkzOTM4ODk1LC0wLjA0Mjk2 + MjU3NywwLjAzNDA1ODQxNCwwLjAzMzM5MDYsLTAuMDU2OTg2NjMsLTAuMDE1NTgyMjgxLDAuMDM3 + MTc0ODcsMC4wNDI5NjI1NzcsLTAuMTcwMDY5NDcsMC4wNDI1MTczNjgsLTAuMDM0NTAzNjIsLTAu + MDcyMTIzNywtMC4wMDYyODg1NjM0LC0wLjA2MjMyOTEyNSwtMC4wMTYzNjEzOTUsMC4wNTk2NTc4 + NzUsMC4wMjAwMzQzNiwtMC4wMjc3MTQyLC0wLjEzODkwNDkxLC0wLjAzNDcyNjIzLC0wLjAxNjgw + NjYwNCwwLjAzOTQwMDkxMywwLjAwMzE5OTkzMjYsMC4wNDU2MzM4MjMsLTAuMDQzNDA3NzgzLDAu + MDUwMDg1OTAyLDAuMDI3MzgwMjkzLDAuMDg1OTI1MTU0LC0wLjA2Mjc3NDMzLC0wLjA3NzQ2NjIs + LTAuMDY5NDUyNDYsLTAuMjE2MzcxMTIsMC4wMzEzODcxNjUsMC4wODA1ODI2NTYsMC4xMTM5NzMy + NiwwLjAwMTI3MzAxNjcsLTAuMDA1NDI1OTczLDAuMTAxNTA3NDMsMC4xMjQ2NTgyNSwwLjAwOTEy + Njc2NSwtMC4wNDQ3NDM0MDQsMC4wMzAwNTE1NDIsMC4wOTcwNTUzNSwwLjE5MDU0OTA1LC0wLjAy + NTgyMjA2NiwwLjAwNDAwNjg3MjQsMC4wNjU4OTA3OSwwLjAxNjM2MTM5NSwwLjAxNzE0MDUxLDAu + MDQ3MTkyMDUyLC0wLjAxNTU4MjI4MSwtMC4wNTc4NzcwNDUsMC4wMDc3OTExNDA2LDAuMDUyOTc5 + NzU2LDAuMDY5MDA3MjQsMC4wMzQwNTg0MTQsMC4wMzg3MzMxLDAuMDI2MDQ0NjcsMC4wMzIyNzc1 + ODQsLTAuMDI4NzE1OTE4LC0wLjA0MDczNjUzNCwwLjA5MTcxMjg1LDAuMDEwNDA2NzM4LDAuMDY5 + MDA3MjQsMC4wMTQ2OTE4NjYsMC4wMTM0Njc1NDMsMC4wMDcxNzg5Nzk1LDAuMDM4Mjg3ODkzLC0w + LjAyOTE2MTEyNSwtMC4wNTA3NTM3MTYsMC4wMzI3MjI3OSwtMC4wMjY4MjM3ODMsLTAuMDY1MDAw + MzcsLTAuMDI2MTU1OTczLDAuMDQxODQ5NTU0LDAuMDA4NTcwMjU1LC0wLjE2NzM5ODIxLDAuMDYy + Nzc0MzMsLTAuMDYzNjY0NzUsMC4wMDA0MzEyOTUyNSwwLjA3ODM1NjYxNiwtMC4wMzQwNTg0MTQs + LTAuMDEyNTc3MTI3LC0wLjAyNzcxNDIsMC4wMzMxNjgsMC4wMzI3MjI3OSwtMC4wMjkxNjExMjUs + LTAuMDIwNzAyMTc0LC0wLjEwNjg0OTkzLC0wLjAyNDA0MTIzNCwtMC4wMjYzNzg1NzYsMC4wNTY5 + ODY2MywtMC4xMDAxNzE4MDQsLTAuMDA4MDY5Mzk2LC0wLjAyMjkyODIxMiwwLjAwOTU3MTk3Mywt + MC4wNjQxMDk5NiwtMC4wNTk2NTc4NzUsLTAuMDQxNjI2OTUzLC0wLjAzNzYyMDA4LDAuMDA1NDI1 + OTczLC0wLjAwNjY3ODEyMDcsLTAuMDQzMTg1MTgsLTAuMDQ0OTY2MDEyLDAuMDQzNjMwMzg4LC0w + LjE5NTAwMTEzLC0wLjAwOTA3MTExNCwtMC4wNDU4NTY0MjQsLTAuMDY4MTE2ODMsMC4wODE0NzMw + NywtMC4xNDY5MTg2NSwwLjEwOTA3NTk3LC0wLjEwMzczMzQ3LDAuMDIwNTkwODcsLTAuMDAxMjkz + ODg1OSwwLjAyNDQ4NjQ0MSwtMC4xNTEzNzA3MywtMC4wNTA1MzExMDgsLTAuMDY3MjI2NDIsLTAu + MDMzNjEzMjEsLTAuMTQ2OTE4NjUsMC4wOTI2MDMyNywtMC4wMzk2MjM1MTQsMC4wNjE0Mzg3MSww + LjA4MTQ3MzA3LDAuMDcwNzg4MDgsLTAuMDQ2MDc5MDMyLC0wLjExMzk3MzI2LDAuMDEwMDE3MTgs + MC4wNzAzNDI4NywwLjEzMzU2MjQsLTAuMDMxODMyMzc1LDAuMDAxNjQxNzA0NiwwLjAzNDk0ODgz + LC0wLjEyMDIwNjE3LDAuMDA3NDU3MjM0NCwtMC4wMzMzOTA2LDAuMDM5ODQ2MTIsMC4wMjA1OTA4 + NywtMC4wMDMzOTQ3MTEzLC0wLjA5NTcxOTcyNSwwLjEwNTA2OTA5LC0wLjAxOTE0Mzk0NiwwLjAy + NDU5Nzc0NCwwLjA1MTg2NjczNiwwLjAzNDI4MTAyLC0wLjA1ODMyMjI1LDAuMDQ4NzUwMjgsLTAu + MDE1MzU5Njc3LC0wLjAwNjI4ODU2MzQsLTAuMDE3ODA4MzIyLC0wLjA2ODExNjgzLC0wLjExOTMx + NTc1LDAuMDA0MDkwMzQ4NywwLjA0NDI5ODE5OCwwLjA1MDUzMTEwOCwtMC4wNTc0MzE4MzYsMC4w + MTU4MDQ4ODUsLTAuMDM1MTcxNDM0LDAuMDE3ODA4MzIyLC0wLjAxNDkxNDQ2OSwtMC4wMzQ1MDM2 + MiwwLjA5MTI2NzY0NSwwLjA5Nzk0NTc2NSwwLjAwMDQyNjA3OCwtMC4wMDg2ODE1NTcsLTAuMDMw + NzE5MzU1LC0wLjEwOTk2NjM4LC0wLjA0MTE4MTc0LC0wLjAxMTkwOTMxNSwtMC4wMzA5NDE5NTgs + MC4wNTE2NDQxMywtMC4wMzIyNzc1ODQsMC4wMTIyNDMyMjEsMC4xMDk1MjExNywtMC4xMDI4NDMw + NSwtMC4wNTI1MzQ1NDcsLTAuMTA0MTc4NjgsLTAuMDExNDA4NDU2LDAuMDAxMzQ5NTM2OCwtMC4w + NjI3NzQzMywtMC4wMjgyNzA3MSwwLjAxMTc0MjM2MiwwLjAwMzkyMzM5NTYsMC4wMDk3Mzg5MjUs + LTAuMDIwMjU2OTY2LC0wLjAyNTcxMDc2MywtMC4wNTA3NTM3MTYsLTAuMDEwMjk1NDM1LC0wLjAz + MzgzNTgxLDAuMDQxMTgxNzQsLTAuMDA0Mzk2NDI5MywtMC4wMzY3Mjk2NjQsLTAuMTMzNTYyNCww + LjA1NjA5NjIxLDAuMDQ3ODU5ODYyLDAuMDYxODgzOTE1LC0wLjAwNDMxMjk1MjUsMC4wMTgyNTM1 + MywtMC4xMTkzMTU3NSwwLjAwNDc4NTk4NjYsMC4wMDc4NDY3OTEsMC4wMjQ4MjAzNDgsLTAuMDQ1 + ODU2NDI0LC0wLjA1Mjc1NzE1LC0wLjAwMjk0OTUwMzMsLTAuMDUwNTMxMTA4LDAuMDg1MDM0NzM2 + LDAuMTA3NzQwMzQsMC4wNTE4NjY3MzYsLTAuMDI3MzgwMjkzLC0wLjAzNTE3MTQzNCwwLjAxNTY5 + MzU4MywtMC4wNjMyMTk1NCwtMC4wNTI5Nzk3NTYsMC4wNTc0MzE4MzYsMC4wMDQzOTY0MjkzLDAu + MTM4MDE0NDgsLTAuMDQyNzM5OTcyLDAuMDEyNTc3MTI3LC0wLjA1Mjc1NzE1LC0wLjAyOTgyODkz + OCwwLjA0MzQwNzc4MywtMC4wMzA0OTY3NSwtMC4wNTc0MzE4MzYsLTAuMDUwMDg1OTAyLDAuMDg2 + ODE1NTY2LC0wLjAwMzgxMjA5MzgsLTAuMTEyMTkyNDIsMC4xMTkzMTU3NSwwLjA1OTIxMjY3LDAu + MDU1MjA1NzkyLDAuMDcxMjMzMjksLTAuMDU4MzIyMjUsMC4wMTA5MDc1OTcsLTAuMDMyOTQ1Mzk1 + LC0wLjA1MTE5ODkyMiwtMC4wNzAzNDI4NywtMC4wMjY4MjM3ODMsLTAuMDI1NTk5NDYxLC0wLjA0 + ODUyNzY3NywwLjAzNDcyNjIzLC0wLjAwNTAwODU5LDAuMDM4MDY1MjksMC4wMzcxNzQ4NywwLjA1 + MzIwMjM2LDAuMDk0Mzg0MTA0LC0wLjA4NTkyNTE1NCwwLjAzNDcyNjIzLC0wLjA3NTI0MDE2LC0w + LjAwMDc1MTI4ODU1LC0wLjAzNjk1MjI3LDAuMDA2NDgzMzQyLDAuMDQwMjkxMzI4LDAuMDM3Mzk3 + NDc0XV19LCJtZXRhIjp7ImFwaV92ZXJzaW9uIjp7InZlcnNpb24iOiIyIn0sImJpbGxlZF91bml0 + cyI6eyJpbnB1dF90b2tlbnMiOjE3LCJpbWFnZV90b2tlbnMiOjB9fSwicmVzcG9uc2VfdHlwZSI6 + ImVtYmVkZGluZ3NfYnlfdHlwZSJ9 diff --git a/tests/cassettes/embeddings/recorded/cohere/classification.yaml b/tests/cassettes/embeddings/recorded/cohere/classification.yaml new file mode 100644 index 0000000..f1e7274 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/classification.yaml @@ -0,0 +1,352 @@ +timestamp: '2026-11-03T17:02:23.867246+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: embed-v4.0 + texts: + - Penguins live in the Southern Hemisphere. + input_type: classification + truncate: END + embedding_types: + - float + output_dimension: 256 +response: + status_code: 200 + headers: + content-type: application/json + body: + id: 8eb6cc1a-5102-434e-bc98-a2cbff28b619 + texts: + - Penguins live in the Southern Hemisphere. + embeddings: + float: + - - -0.03070113 + - -0.035560302 + - -0.06670318 + - 0.0072887572 + - -0.0150192585 + - -0.10999397 + - -0.02297063 + - 0.0016496335 + - -0.0048315628 + - -0.07067886 + - 0.00042793842 + - -0.04549952 + - -0.028492417 + - 0.033130717 + - -0.0029541554 + - -0.004500256 + - 0.09497472 + - 0.025621088 + - -0.1091105 + - -0.14489166 + - -0.013914901 + - 0.00039860394 + - 0.034455944 + - -0.012037494 + - 0.017007101 + - 0.017007101 + - 0.057868317 + - -0.00013718188 + - -0.03644379 + - 0.053671762 + - -0.02451673 + - -0.05389263 + - -0.13605681 + - 0.034455944 + - 0.03644379 + - 0.09144078 + - -0.040861215 + - -0.018221894 + - 0.112644434 + - 0.13428983 + - 0.016786229 + - -0.042186446 + - 0.038652502 + - 0.021424528 + - 0.1607944 + - -0.064052716 + - -0.032026358 + - 0.10160086 + - 0.10425132 + - -0.005908311 + - 0.008669204 + - -0.0015530023 + - -0.036885533 + - 0.08746509 + - 0.012479236 + - 0.07377107 + - 0.05345089 + - 0.07023712 + - 0.012479236 + - 0.06361098 + - -0.044836905 + - 0.047266487 + - 0.03533943 + - -0.017669715 + - 0.08260592 + - -0.021314096 + - -0.08083895 + - 0.057868317 + - 0.023412373 + - -0.05035869 + - -0.004445038 + - 0.018884508 + - -0.022860195 + - -0.048149977 + - -0.041082088 + - 0.029817645 + - 0.019767994 + - -0.020541044 + - 0.022860195 + - -0.046824746 + - 0.04549952 + - 0.039756857 + - -0.082164176 + - 0.005715049 + - -0.057868317 + - -0.016675794 + - 0.00593592 + - -0.0324681 + - -0.05963529 + - -0.038873374 + - 0.025841957 + - -0.052788273 + - -0.027498493 + - -0.16962928 + - -0.051904786 + - -0.006764188 + - -0.043511674 + - 0.045057774 + - -0.037106402 + - -0.053009145 + - -0.10513481 + - -0.0054665683 + - -0.03644379 + - 0.001456371 + - 0.019215815 + - 0.014467079 + - -0.11308618 + - -0.048812587 + - -0.03070113 + - 0.011927058 + - 0.0036995967 + - -0.23324023 + - 0.064052716 + - -0.07067886 + - -0.0065157074 + - 0.017227972 + - 0.014908822 + - 0.024185423 + - 0.009387036 + - -0.053009145 + - 0.035560302 + - -0.18199807 + - 0.06670318 + - -0.040419474 + - 0.016123615 + - 0.06361098 + - 0.09364949 + - 0.007840936 + - -0.0803972 + - -0.03489769 + - 0.035560302 + - 0.11308618 + - -0.009607907 + - 0.06140226 + - 0.027498493 + - -0.09144078 + - 0.025510652 + - -0.0022363234 + - 0.054996986 + - 0.054996986 + - 0.008558768 + - -0.15372652 + - 0.15107606 + - 0.0096631255 + - 0.05035869 + - 0.02451673 + - 0.055880476 + - -0.053009145 + - 0.028050672 + - 0.040640343 + - 0.08746509 + - -0.056984834 + - -0.041082088 + - -0.024958473 + - 0.048149977 + - 0.0649362 + - 0.028713286 + - 0.03533943 + - 0.055659603 + - 0.0026228484 + - -0.005024825 + - -0.014246209 + - -0.048370846 + - 0.08613986 + - 0.10248435 + - -0.106460035 + - 0.025731524 + - -0.007951372 + - -0.012976197 + - -0.13870727 + - -0.06979538 + - -0.011264443 + - 0.033572458 + - -0.09983389 + - 0.008890076 + - 0.019767994 + - -0.023081064 + - -0.020209737 + - -0.050137818 + - -0.012865762 + - 0.017007101 + - 0.03224723 + - -0.08746509 + - 0.057868317 + - -0.078188494 + - -0.018663635 + - -0.10778526 + - -0.061844002 + - -0.05963529 + - -0.017448844 + - 0.0077305003 + - 0.14135772 + - -0.0026090438 + - -0.06140226 + - -0.15195955 + - 0.07553803 + - -0.054334376 + - -0.012534454 + - 0.054113504 + - -0.07774675 + - -0.04925433 + - 0.06449446 + - -0.011761405 + - -0.039094247 + - 0.051242176 + - 0.003975686 + - 0.061844002 + - -0.10336783 + - 0.049916945 + - 0.1298724 + - 0.011540532 + - -0.010215304 + - -0.010270522 + - 0.064052716 + - -0.04748736 + - -0.09144078 + - 0.07332932 + - 0.008393114 + - 0.09674169 + - -0.05080043 + - 0.06140226 + - -0.1091105 + - 0.07244583 + - 0.14047424 + - -0.057868317 + - -0.0649362 + - -0.045278646 + - 0.0803972 + - 0.014467079 + - -0.15284304 + - 0.07288758 + - 0.014246209 + - 0.07642152 + - 0.010546612 + - -0.06537794 + - -0.013031415 + - 0.01965756 + - -0.059193548 + - -0.07377107 + - 0.07332932 + - -0.056984834 + - -0.023633244 + - 0.0247376 + - 0.028271545 + - -0.021424528 + - -0.012865762 + - 0.05389263 + - 0.06581969 + - -0.10425132 + - 0.041082088 + - -0.05389263 + - -0.0034787252 + - 0.018001024 + - 0.02120366 + - 0.01965756 + - -0.05963529 + meta: + api_version: + version: '2' + billed_units: + input_tokens: 8 + image_tokens: 0 + response_type: embeddings_by_type + body_bytes: !!binary | + eyJpZCI6IjhlYjZjYzFhLTUxMDItNDM0ZS1iYzk4LWEyY2JmZjI4YjYxOSIsInRleHRzIjpbIlBl + bmd1aW5zIGxpdmUgaW4gdGhlIFNvdXRoZXJuIEhlbWlzcGhlcmUuIl0sImVtYmVkZGluZ3MiOnsi + ZmxvYXQiOltbLTAuMDMwNzAxMTMsLTAuMDM1NTYwMzAyLC0wLjA2NjcwMzE4LDAuMDA3Mjg4NzU3 + MiwtMC4wMTUwMTkyNTg1LC0wLjEwOTk5Mzk3LC0wLjAyMjk3MDYzLDAuMDAxNjQ5NjMzNSwtMC4w + MDQ4MzE1NjI4LC0wLjA3MDY3ODg2LDAuMDAwNDI3OTM4NDIsLTAuMDQ1NDk5NTIsLTAuMDI4NDky + NDE3LDAuMDMzMTMwNzE3LC0wLjAwMjk1NDE1NTQsLTAuMDA0NTAwMjU2LDAuMDk0OTc0NzIsMC4w + MjU2MjEwODgsLTAuMTA5MTEwNSwtMC4xNDQ4OTE2NiwtMC4wMTM5MTQ5MDEsMC4wMDAzOTg2MDM5 + NCwwLjAzNDQ1NTk0NCwtMC4wMTIwMzc0OTQsMC4wMTcwMDcxMDEsMC4wMTcwMDcxMDEsMC4wNTc4 + NjgzMTcsLTAuMDAwMTM3MTgxODgsLTAuMDM2NDQzNzksMC4wNTM2NzE3NjIsLTAuMDI0NTE2NzMs + LTAuMDUzODkyNjMsLTAuMTM2MDU2ODEsMC4wMzQ0NTU5NDQsMC4wMzY0NDM3OSwwLjA5MTQ0MDc4 + LC0wLjA0MDg2MTIxNSwtMC4wMTgyMjE4OTQsMC4xMTI2NDQ0MzQsMC4xMzQyODk4MywwLjAxNjc4 + NjIyOSwtMC4wNDIxODY0NDYsMC4wMzg2NTI1MDIsMC4wMjE0MjQ1MjgsMC4xNjA3OTQ0LC0wLjA2 + NDA1MjcxNiwtMC4wMzIwMjYzNTgsMC4xMDE2MDA4NiwwLjEwNDI1MTMyLC0wLjAwNTkwODMxMSww + LjAwODY2OTIwNCwtMC4wMDE1NTMwMDIzLC0wLjAzNjg4NTUzMywwLjA4NzQ2NTA5LDAuMDEyNDc5 + MjM2LDAuMDczNzcxMDcsMC4wNTM0NTA4OSwwLjA3MDIzNzEyLDAuMDEyNDc5MjM2LDAuMDYzNjEw + OTgsLTAuMDQ0ODM2OTA1LDAuMDQ3MjY2NDg3LDAuMDM1MzM5NDMsLTAuMDE3NjY5NzE1LDAuMDgy + NjA1OTIsLTAuMDIxMzE0MDk2LC0wLjA4MDgzODk1LDAuMDU3ODY4MzE3LDAuMDIzNDEyMzczLC0w + LjA1MDM1ODY5LC0wLjAwNDQ0NTAzOCwwLjAxODg4NDUwOCwtMC4wMjI4NjAxOTUsLTAuMDQ4MTQ5 + OTc3LC0wLjA0MTA4MjA4OCwwLjAyOTgxNzY0NSwwLjAxOTc2Nzk5NCwtMC4wMjA1NDEwNDQsMC4w + MjI4NjAxOTUsLTAuMDQ2ODI0NzQ2LDAuMDQ1NDk5NTIsMC4wMzk3NTY4NTcsLTAuMDgyMTY0MTc2 + LDAuMDA1NzE1MDQ5LC0wLjA1Nzg2ODMxNywtMC4wMTY2NzU3OTQsMC4wMDU5MzU5MiwtMC4wMzI0 + NjgxLC0wLjA1OTYzNTI5LC0wLjAzODg3MzM3NCwwLjAyNTg0MTk1NywtMC4wNTI3ODgyNzMsLTAu + MDI3NDk4NDkzLC0wLjE2OTYyOTI4LC0wLjA1MTkwNDc4NiwtMC4wMDY3NjQxODgsLTAuMDQzNTEx + Njc0LDAuMDQ1MDU3Nzc0LC0wLjAzNzEwNjQwMiwtMC4wNTMwMDkxNDUsLTAuMTA1MTM0ODEsLTAu + MDA1NDY2NTY4MywtMC4wMzY0NDM3OSwwLjAwMTQ1NjM3MSwwLjAxOTIxNTgxNSwwLjAxNDQ2NzA3 + OSwtMC4xMTMwODYxOCwtMC4wNDg4MTI1ODcsLTAuMDMwNzAxMTMsMC4wMTE5MjcwNTgsMC4wMDM2 + OTk1OTY3LC0wLjIzMzI0MDIzLDAuMDY0MDUyNzE2LC0wLjA3MDY3ODg2LC0wLjAwNjUxNTcwNzQs + MC4wMTcyMjc5NzIsMC4wMTQ5MDg4MjIsMC4wMjQxODU0MjMsMC4wMDkzODcwMzYsLTAuMDUzMDA5 + MTQ1LDAuMDM1NTYwMzAyLC0wLjE4MTk5ODA3LDAuMDY2NzAzMTgsLTAuMDQwNDE5NDc0LDAuMDE2 + MTIzNjE1LDAuMDYzNjEwOTgsMC4wOTM2NDk0OSwwLjAwNzg0MDkzNiwtMC4wODAzOTcyLC0wLjAz + NDg5NzY5LDAuMDM1NTYwMzAyLDAuMTEzMDg2MTgsLTAuMDA5NjA3OTA3LDAuMDYxNDAyMjYsMC4w + Mjc0OTg0OTMsLTAuMDkxNDQwNzgsMC4wMjU1MTA2NTIsLTAuMDAyMjM2MzIzNCwwLjA1NDk5Njk4 + NiwwLjA1NDk5Njk4NiwwLjAwODU1ODc2OCwtMC4xNTM3MjY1MiwwLjE1MTA3NjA2LDAuMDA5NjYz + MTI1NSwwLjA1MDM1ODY5LDAuMDI0NTE2NzMsMC4wNTU4ODA0NzYsLTAuMDUzMDA5MTQ1LDAuMDI4 + MDUwNjcyLDAuMDQwNjQwMzQzLDAuMDg3NDY1MDksLTAuMDU2OTg0ODM0LC0wLjA0MTA4MjA4OCwt + MC4wMjQ5NTg0NzMsMC4wNDgxNDk5NzcsMC4wNjQ5MzYyLDAuMDI4NzEzMjg2LDAuMDM1MzM5NDMs + MC4wNTU2NTk2MDMsMC4wMDI2MjI4NDg0LC0wLjAwNTAyNDgyNSwtMC4wMTQyNDYyMDksLTAuMDQ4 + MzcwODQ2LDAuMDg2MTM5ODYsMC4xMDI0ODQzNSwtMC4xMDY0NjAwMzUsMC4wMjU3MzE1MjQsLTAu + MDA3OTUxMzcyLC0wLjAxMjk3NjE5NywtMC4xMzg3MDcyNywtMC4wNjk3OTUzOCwtMC4wMTEyNjQ0 + NDMsMC4wMzM1NzI0NTgsLTAuMDk5ODMzODksMC4wMDg4OTAwNzYsMC4wMTk3Njc5OTQsLTAuMDIz + MDgxMDY0LC0wLjAyMDIwOTczNywtMC4wNTAxMzc4MTgsLTAuMDEyODY1NzYyLDAuMDE3MDA3MTAx + LDAuMDMyMjQ3MjMsLTAuMDg3NDY1MDksMC4wNTc4NjgzMTcsLTAuMDc4MTg4NDk0LC0wLjAxODY2 + MzYzNSwtMC4xMDc3ODUyNiwtMC4wNjE4NDQwMDIsLTAuMDU5NjM1MjksLTAuMDE3NDQ4ODQ0LDAu + MDA3NzMwNTAwMywwLjE0MTM1NzcyLC0wLjAwMjYwOTA0MzgsLTAuMDYxNDAyMjYsLTAuMTUxOTU5 + NTUsMC4wNzU1MzgwMywtMC4wNTQzMzQzNzYsLTAuMDEyNTM0NDU0LDAuMDU0MTEzNTA0LC0wLjA3 + Nzc0Njc1LC0wLjA0OTI1NDMzLDAuMDY0NDk0NDYsLTAuMDExNzYxNDA1LC0wLjAzOTA5NDI0Nyww + LjA1MTI0MjE3NiwwLjAwMzk3NTY4NiwwLjA2MTg0NDAwMiwtMC4xMDMzNjc4MywwLjA0OTkxNjk0 + NSwwLjEyOTg3MjQsMC4wMTE1NDA1MzIsLTAuMDEwMjE1MzA0LC0wLjAxMDI3MDUyMiwwLjA2NDA1 + MjcxNiwtMC4wNDc0ODczNiwtMC4wOTE0NDA3OCwwLjA3MzMyOTMyLDAuMDA4MzkzMTE0LDAuMDk2 + NzQxNjksLTAuMDUwODAwNDMsMC4wNjE0MDIyNiwtMC4xMDkxMTA1LDAuMDcyNDQ1ODMsMC4xNDA0 + NzQyNCwtMC4wNTc4NjgzMTcsLTAuMDY0OTM2MiwtMC4wNDUyNzg2NDYsMC4wODAzOTcyLDAuMDE0 + NDY3MDc5LC0wLjE1Mjg0MzA0LDAuMDcyODg3NTgsMC4wMTQyNDYyMDksMC4wNzY0MjE1MiwwLjAx + MDU0NjYxMiwtMC4wNjUzNzc5NCwtMC4wMTMwMzE0MTUsMC4wMTk2NTc1NiwtMC4wNTkxOTM1NDgs + LTAuMDczNzcxMDcsMC4wNzMzMjkzMiwtMC4wNTY5ODQ4MzQsLTAuMDIzNjMzMjQ0LDAuMDI0NzM3 + NiwwLjAyODI3MTU0NSwtMC4wMjE0MjQ1MjgsLTAuMDEyODY1NzYyLDAuMDUzODkyNjMsMC4wNjU4 + MTk2OSwtMC4xMDQyNTEzMiwwLjA0MTA4MjA4OCwtMC4wNTM4OTI2MywtMC4wMDM0Nzg3MjUyLDAu + MDE4MDAxMDI0LDAuMDIxMjAzNjYsMC4wMTk2NTc1NiwtMC4wNTk2MzUyOV1dfSwibWV0YSI6eyJh + cGlfdmVyc2lvbiI6eyJ2ZXJzaW9uIjoiMiJ9LCJiaWxsZWRfdW5pdHMiOnsiaW5wdXRfdG9rZW5z + Ijo4LCJpbWFnZV90b2tlbnMiOjB9fSwicmVzcG9uc2VfdHlwZSI6ImVtYmVkZGluZ3NfYnlfdHlw + ZSJ9 diff --git a/tests/cassettes/embeddings/recorded/cohere/clustering.yaml b/tests/cassettes/embeddings/recorded/cohere/clustering.yaml new file mode 100644 index 0000000..9a9010b --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/clustering.yaml @@ -0,0 +1,352 @@ +timestamp: '2026-11-03T17:02:24.204214+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: embed-v4.0 + texts: + - Penguins live in the Southern Hemisphere. + input_type: clustering + truncate: START + embedding_types: + - float + output_dimension: 256 +response: + status_code: 200 + headers: + content-type: application/json + body: + id: 1db478ec-cef2-45e2-9e61-c7d132d051b2 + texts: + - Penguins live in the Southern Hemisphere. + embeddings: + float: + - - -0.03070113 + - -0.035560302 + - -0.06670318 + - 0.0072887572 + - -0.0150192585 + - -0.10999397 + - -0.02297063 + - 0.0016496335 + - -0.0048315628 + - -0.07067886 + - 0.00042793842 + - -0.04549952 + - -0.028492417 + - 0.033130717 + - -0.0029541554 + - -0.004500256 + - 0.09497472 + - 0.025621088 + - -0.1091105 + - -0.14489166 + - -0.013914901 + - 0.00039860394 + - 0.034455944 + - -0.012037494 + - 0.017007101 + - 0.017007101 + - 0.057868317 + - -0.00013718188 + - -0.03644379 + - 0.053671762 + - -0.02451673 + - -0.05389263 + - -0.13605681 + - 0.034455944 + - 0.03644379 + - 0.09144078 + - -0.040861215 + - -0.018221894 + - 0.112644434 + - 0.13428983 + - 0.016786229 + - -0.042186446 + - 0.038652502 + - 0.021424528 + - 0.1607944 + - -0.064052716 + - -0.032026358 + - 0.10160086 + - 0.10425132 + - -0.005908311 + - 0.008669204 + - -0.0015530023 + - -0.036885533 + - 0.08746509 + - 0.012479236 + - 0.07377107 + - 0.05345089 + - 0.07023712 + - 0.012479236 + - 0.06361098 + - -0.044836905 + - 0.047266487 + - 0.03533943 + - -0.017669715 + - 0.08260592 + - -0.021314096 + - -0.08083895 + - 0.057868317 + - 0.023412373 + - -0.05035869 + - -0.004445038 + - 0.018884508 + - -0.022860195 + - -0.048149977 + - -0.041082088 + - 0.029817645 + - 0.019767994 + - -0.020541044 + - 0.022860195 + - -0.046824746 + - 0.04549952 + - 0.039756857 + - -0.082164176 + - 0.005715049 + - -0.057868317 + - -0.016675794 + - 0.00593592 + - -0.0324681 + - -0.05963529 + - -0.038873374 + - 0.025841957 + - -0.052788273 + - -0.027498493 + - -0.16962928 + - -0.051904786 + - -0.006764188 + - -0.043511674 + - 0.045057774 + - -0.037106402 + - -0.053009145 + - -0.10513481 + - -0.0054665683 + - -0.03644379 + - 0.001456371 + - 0.019215815 + - 0.014467079 + - -0.11308618 + - -0.048812587 + - -0.03070113 + - 0.011927058 + - 0.0036995967 + - -0.23324023 + - 0.064052716 + - -0.07067886 + - -0.0065157074 + - 0.017227972 + - 0.014908822 + - 0.024185423 + - 0.009387036 + - -0.053009145 + - 0.035560302 + - -0.18199807 + - 0.06670318 + - -0.040419474 + - 0.016123615 + - 0.06361098 + - 0.09364949 + - 0.007840936 + - -0.0803972 + - -0.03489769 + - 0.035560302 + - 0.11308618 + - -0.009607907 + - 0.06140226 + - 0.027498493 + - -0.09144078 + - 0.025510652 + - -0.0022363234 + - 0.054996986 + - 0.054996986 + - 0.008558768 + - -0.15372652 + - 0.15107606 + - 0.0096631255 + - 0.05035869 + - 0.02451673 + - 0.055880476 + - -0.053009145 + - 0.028050672 + - 0.040640343 + - 0.08746509 + - -0.056984834 + - -0.041082088 + - -0.024958473 + - 0.048149977 + - 0.0649362 + - 0.028713286 + - 0.03533943 + - 0.055659603 + - 0.0026228484 + - -0.005024825 + - -0.014246209 + - -0.048370846 + - 0.08613986 + - 0.10248435 + - -0.106460035 + - 0.025731524 + - -0.007951372 + - -0.012976197 + - -0.13870727 + - -0.06979538 + - -0.011264443 + - 0.033572458 + - -0.09983389 + - 0.008890076 + - 0.019767994 + - -0.023081064 + - -0.020209737 + - -0.050137818 + - -0.012865762 + - 0.017007101 + - 0.03224723 + - -0.08746509 + - 0.057868317 + - -0.078188494 + - -0.018663635 + - -0.10778526 + - -0.061844002 + - -0.05963529 + - -0.017448844 + - 0.0077305003 + - 0.14135772 + - -0.0026090438 + - -0.06140226 + - -0.15195955 + - 0.07553803 + - -0.054334376 + - -0.012534454 + - 0.054113504 + - -0.07774675 + - -0.04925433 + - 0.06449446 + - -0.011761405 + - -0.039094247 + - 0.051242176 + - 0.003975686 + - 0.061844002 + - -0.10336783 + - 0.049916945 + - 0.1298724 + - 0.011540532 + - -0.010215304 + - -0.010270522 + - 0.064052716 + - -0.04748736 + - -0.09144078 + - 0.07332932 + - 0.008393114 + - 0.09674169 + - -0.05080043 + - 0.06140226 + - -0.1091105 + - 0.07244583 + - 0.14047424 + - -0.057868317 + - -0.0649362 + - -0.045278646 + - 0.0803972 + - 0.014467079 + - -0.15284304 + - 0.07288758 + - 0.014246209 + - 0.07642152 + - 0.010546612 + - -0.06537794 + - -0.013031415 + - 0.01965756 + - -0.059193548 + - -0.07377107 + - 0.07332932 + - -0.056984834 + - -0.023633244 + - 0.0247376 + - 0.028271545 + - -0.021424528 + - -0.012865762 + - 0.05389263 + - 0.06581969 + - -0.10425132 + - 0.041082088 + - -0.05389263 + - -0.0034787252 + - 0.018001024 + - 0.02120366 + - 0.01965756 + - -0.05963529 + meta: + api_version: + version: '2' + billed_units: + input_tokens: 8 + image_tokens: 0 + response_type: embeddings_by_type + body_bytes: !!binary | + eyJpZCI6IjFkYjQ3OGVjLWNlZjItNDVlMi05ZTYxLWM3ZDEzMmQwNTFiMiIsInRleHRzIjpbIlBl + bmd1aW5zIGxpdmUgaW4gdGhlIFNvdXRoZXJuIEhlbWlzcGhlcmUuIl0sImVtYmVkZGluZ3MiOnsi + ZmxvYXQiOltbLTAuMDMwNzAxMTMsLTAuMDM1NTYwMzAyLC0wLjA2NjcwMzE4LDAuMDA3Mjg4NzU3 + MiwtMC4wMTUwMTkyNTg1LC0wLjEwOTk5Mzk3LC0wLjAyMjk3MDYzLDAuMDAxNjQ5NjMzNSwtMC4w + MDQ4MzE1NjI4LC0wLjA3MDY3ODg2LDAuMDAwNDI3OTM4NDIsLTAuMDQ1NDk5NTIsLTAuMDI4NDky + NDE3LDAuMDMzMTMwNzE3LC0wLjAwMjk1NDE1NTQsLTAuMDA0NTAwMjU2LDAuMDk0OTc0NzIsMC4w + MjU2MjEwODgsLTAuMTA5MTEwNSwtMC4xNDQ4OTE2NiwtMC4wMTM5MTQ5MDEsMC4wMDAzOTg2MDM5 + NCwwLjAzNDQ1NTk0NCwtMC4wMTIwMzc0OTQsMC4wMTcwMDcxMDEsMC4wMTcwMDcxMDEsMC4wNTc4 + NjgzMTcsLTAuMDAwMTM3MTgxODgsLTAuMDM2NDQzNzksMC4wNTM2NzE3NjIsLTAuMDI0NTE2NzMs + LTAuMDUzODkyNjMsLTAuMTM2MDU2ODEsMC4wMzQ0NTU5NDQsMC4wMzY0NDM3OSwwLjA5MTQ0MDc4 + LC0wLjA0MDg2MTIxNSwtMC4wMTgyMjE4OTQsMC4xMTI2NDQ0MzQsMC4xMzQyODk4MywwLjAxNjc4 + NjIyOSwtMC4wNDIxODY0NDYsMC4wMzg2NTI1MDIsMC4wMjE0MjQ1MjgsMC4xNjA3OTQ0LC0wLjA2 + NDA1MjcxNiwtMC4wMzIwMjYzNTgsMC4xMDE2MDA4NiwwLjEwNDI1MTMyLC0wLjAwNTkwODMxMSww + LjAwODY2OTIwNCwtMC4wMDE1NTMwMDIzLC0wLjAzNjg4NTUzMywwLjA4NzQ2NTA5LDAuMDEyNDc5 + MjM2LDAuMDczNzcxMDcsMC4wNTM0NTA4OSwwLjA3MDIzNzEyLDAuMDEyNDc5MjM2LDAuMDYzNjEw + OTgsLTAuMDQ0ODM2OTA1LDAuMDQ3MjY2NDg3LDAuMDM1MzM5NDMsLTAuMDE3NjY5NzE1LDAuMDgy + NjA1OTIsLTAuMDIxMzE0MDk2LC0wLjA4MDgzODk1LDAuMDU3ODY4MzE3LDAuMDIzNDEyMzczLC0w + LjA1MDM1ODY5LC0wLjAwNDQ0NTAzOCwwLjAxODg4NDUwOCwtMC4wMjI4NjAxOTUsLTAuMDQ4MTQ5 + OTc3LC0wLjA0MTA4MjA4OCwwLjAyOTgxNzY0NSwwLjAxOTc2Nzk5NCwtMC4wMjA1NDEwNDQsMC4w + MjI4NjAxOTUsLTAuMDQ2ODI0NzQ2LDAuMDQ1NDk5NTIsMC4wMzk3NTY4NTcsLTAuMDgyMTY0MTc2 + LDAuMDA1NzE1MDQ5LC0wLjA1Nzg2ODMxNywtMC4wMTY2NzU3OTQsMC4wMDU5MzU5MiwtMC4wMzI0 + NjgxLC0wLjA1OTYzNTI5LC0wLjAzODg3MzM3NCwwLjAyNTg0MTk1NywtMC4wNTI3ODgyNzMsLTAu + MDI3NDk4NDkzLC0wLjE2OTYyOTI4LC0wLjA1MTkwNDc4NiwtMC4wMDY3NjQxODgsLTAuMDQzNTEx + Njc0LDAuMDQ1MDU3Nzc0LC0wLjAzNzEwNjQwMiwtMC4wNTMwMDkxNDUsLTAuMTA1MTM0ODEsLTAu + MDA1NDY2NTY4MywtMC4wMzY0NDM3OSwwLjAwMTQ1NjM3MSwwLjAxOTIxNTgxNSwwLjAxNDQ2NzA3 + OSwtMC4xMTMwODYxOCwtMC4wNDg4MTI1ODcsLTAuMDMwNzAxMTMsMC4wMTE5MjcwNTgsMC4wMDM2 + OTk1OTY3LC0wLjIzMzI0MDIzLDAuMDY0MDUyNzE2LC0wLjA3MDY3ODg2LC0wLjAwNjUxNTcwNzQs + MC4wMTcyMjc5NzIsMC4wMTQ5MDg4MjIsMC4wMjQxODU0MjMsMC4wMDkzODcwMzYsLTAuMDUzMDA5 + MTQ1LDAuMDM1NTYwMzAyLC0wLjE4MTk5ODA3LDAuMDY2NzAzMTgsLTAuMDQwNDE5NDc0LDAuMDE2 + MTIzNjE1LDAuMDYzNjEwOTgsMC4wOTM2NDk0OSwwLjAwNzg0MDkzNiwtMC4wODAzOTcyLC0wLjAz + NDg5NzY5LDAuMDM1NTYwMzAyLDAuMTEzMDg2MTgsLTAuMDA5NjA3OTA3LDAuMDYxNDAyMjYsMC4w + Mjc0OTg0OTMsLTAuMDkxNDQwNzgsMC4wMjU1MTA2NTIsLTAuMDAyMjM2MzIzNCwwLjA1NDk5Njk4 + NiwwLjA1NDk5Njk4NiwwLjAwODU1ODc2OCwtMC4xNTM3MjY1MiwwLjE1MTA3NjA2LDAuMDA5NjYz + MTI1NSwwLjA1MDM1ODY5LDAuMDI0NTE2NzMsMC4wNTU4ODA0NzYsLTAuMDUzMDA5MTQ1LDAuMDI4 + MDUwNjcyLDAuMDQwNjQwMzQzLDAuMDg3NDY1MDksLTAuMDU2OTg0ODM0LC0wLjA0MTA4MjA4OCwt + MC4wMjQ5NTg0NzMsMC4wNDgxNDk5NzcsMC4wNjQ5MzYyLDAuMDI4NzEzMjg2LDAuMDM1MzM5NDMs + MC4wNTU2NTk2MDMsMC4wMDI2MjI4NDg0LC0wLjAwNTAyNDgyNSwtMC4wMTQyNDYyMDksLTAuMDQ4 + MzcwODQ2LDAuMDg2MTM5ODYsMC4xMDI0ODQzNSwtMC4xMDY0NjAwMzUsMC4wMjU3MzE1MjQsLTAu + MDA3OTUxMzcyLC0wLjAxMjk3NjE5NywtMC4xMzg3MDcyNywtMC4wNjk3OTUzOCwtMC4wMTEyNjQ0 + NDMsMC4wMzM1NzI0NTgsLTAuMDk5ODMzODksMC4wMDg4OTAwNzYsMC4wMTk3Njc5OTQsLTAuMDIz + MDgxMDY0LC0wLjAyMDIwOTczNywtMC4wNTAxMzc4MTgsLTAuMDEyODY1NzYyLDAuMDE3MDA3MTAx + LDAuMDMyMjQ3MjMsLTAuMDg3NDY1MDksMC4wNTc4NjgzMTcsLTAuMDc4MTg4NDk0LC0wLjAxODY2 + MzYzNSwtMC4xMDc3ODUyNiwtMC4wNjE4NDQwMDIsLTAuMDU5NjM1MjksLTAuMDE3NDQ4ODQ0LDAu + MDA3NzMwNTAwMywwLjE0MTM1NzcyLC0wLjAwMjYwOTA0MzgsLTAuMDYxNDAyMjYsLTAuMTUxOTU5 + NTUsMC4wNzU1MzgwMywtMC4wNTQzMzQzNzYsLTAuMDEyNTM0NDU0LDAuMDU0MTEzNTA0LC0wLjA3 + Nzc0Njc1LC0wLjA0OTI1NDMzLDAuMDY0NDk0NDYsLTAuMDExNzYxNDA1LC0wLjAzOTA5NDI0Nyww + LjA1MTI0MjE3NiwwLjAwMzk3NTY4NiwwLjA2MTg0NDAwMiwtMC4xMDMzNjc4MywwLjA0OTkxNjk0 + NSwwLjEyOTg3MjQsMC4wMTE1NDA1MzIsLTAuMDEwMjE1MzA0LC0wLjAxMDI3MDUyMiwwLjA2NDA1 + MjcxNiwtMC4wNDc0ODczNiwtMC4wOTE0NDA3OCwwLjA3MzMyOTMyLDAuMDA4MzkzMTE0LDAuMDk2 + NzQxNjksLTAuMDUwODAwNDMsMC4wNjE0MDIyNiwtMC4xMDkxMTA1LDAuMDcyNDQ1ODMsMC4xNDA0 + NzQyNCwtMC4wNTc4NjgzMTcsLTAuMDY0OTM2MiwtMC4wNDUyNzg2NDYsMC4wODAzOTcyLDAuMDE0 + NDY3MDc5LC0wLjE1Mjg0MzA0LDAuMDcyODg3NTgsMC4wMTQyNDYyMDksMC4wNzY0MjE1MiwwLjAx + MDU0NjYxMiwtMC4wNjUzNzc5NCwtMC4wMTMwMzE0MTUsMC4wMTk2NTc1NiwtMC4wNTkxOTM1NDgs + LTAuMDczNzcxMDcsMC4wNzMzMjkzMiwtMC4wNTY5ODQ4MzQsLTAuMDIzNjMzMjQ0LDAuMDI0NzM3 + NiwwLjAyODI3MTU0NSwtMC4wMjE0MjQ1MjgsLTAuMDEyODY1NzYyLDAuMDUzODkyNjMsMC4wNjU4 + MTk2OSwtMC4xMDQyNTEzMiwwLjA0MTA4MjA4OCwtMC4wNTM4OTI2MywtMC4wMDM0Nzg3MjUyLDAu + MDE4MDAxMDI0LDAuMDIxMjAzNjYsMC4wMTk2NTc1NiwtMC4wNTk2MzUyOV1dfSwibWV0YSI6eyJh + cGlfdmVyc2lvbiI6eyJ2ZXJzaW9uIjoiMiJ9LCJiaWxsZWRfdW5pdHMiOnsiaW5wdXRfdG9rZW5z + Ijo4LCJpbWFnZV90b2tlbnMiOjB9fSwicmVzcG9uc2VfdHlwZSI6ImVtYmVkZGluZ3NfYnlfdHlw + ZSJ9 diff --git a/tests/cassettes/embeddings/recorded/cohere/invalid-model.yaml b/tests/cassettes/embeddings/recorded/cohere/invalid-model.yaml new file mode 100644 index 0000000..c41a5c0 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/invalid-model.yaml @@ -0,0 +1,29 @@ +timestamp: '2026-11-03T17:02:24.477349+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: molto-invalid-embedding-model + texts: + - Hello + input_type: search_query + embedding_types: + - float +response: + status_code: 404 + headers: + content-type: application/json + body: + id: a6591f25-7776-4c2b-b9ac-e0734c2eb7e1 + message: model 'molto-invalid-embedding-model' not found, make sure the correct + model ID was used and that you have access to the model. + body_bytes: !!binary | + eyJpZCI6ImE2NTkxZjI1LTc3NzYtNGMyYi1iOWFjLWUwNzM0YzJlYjdlMSIsIm1lc3NhZ2UiOiJt + b2RlbCAnbW9sdG8taW52YWxpZC1lbWJlZGRpbmctbW9kZWwnIG5vdCBmb3VuZCwgbWFrZSBzdXJl + IHRoZSBjb3JyZWN0IG1vZGVsIElEIHdhcyB1c2VkIGFuZCB0aGF0IHlvdSBoYXZlIGFjY2VzcyB0 + byB0aGUgbW9kZWwuIn0= diff --git a/tests/cassettes/embeddings/recorded/cohere/manifest.yaml b/tests/cassettes/embeddings/recorded/cohere/manifest.yaml new file mode 100644 index 0000000..655a055 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/manifest.yaml @@ -0,0 +1,28 @@ +recorded_at: '2026-11-03T17:02:24.478988+00:00' +provider: cohere +deployment: direct hosted API +driver: Standalone httpx POST outside pytest, independent of Molto. No retries. +httpx_version: 0.28.1 +base_url: https://api.cohere.com/v2/embed +provenance: faithful live HTTP recording; only authentication header masked and headers + whitelisted; body_bytes preserves original decoded response bytes +schema_reference: https://docs.cohere.com/reference/embed +fixtures: +- file: single-query.yaml + sha256: bfdaf3aae9c65063444987061bdc0007ce4b50b3244ed95efbc5952cfba71a57 + status_code: 200 +- file: batch-document.yaml + sha256: d6d4cd00b12782694e16f162e11b54cf2c8fa186583b614cd533da7d2a8790ab + status_code: 200 +- file: batch-base64.yaml + sha256: 947ff81342062e0103d9797f5331c01c75ee8a4c93dc91db7a23190603ebcd22 + status_code: 200 +- file: classification.yaml + sha256: 8ee355774fd2169be578dc54f475697b47f50502c86d42270eed2373c80a6672 + status_code: 200 +- file: clustering.yaml + sha256: 4726d05955cffe16a10dcd4874249a55783335107c9075321cca46896207d264 + status_code: 200 +- file: invalid-model.yaml + sha256: 0027dc26b96e29ed56ca3a387098981522c1e94fb7a8969e891d9cabb601608e + status_code: 404 diff --git a/tests/cassettes/embeddings/recorded/cohere/single-query.yaml b/tests/cassettes/embeddings/recorded/cohere/single-query.yaml new file mode 100644 index 0000000..5460fad --- /dev/null +++ b/tests/cassettes/embeddings/recorded/cohere/single-query.yaml @@ -0,0 +1,351 @@ +timestamp: '2026-11-03T17:02:22.778917+00:00' +request: + method: POST + url: https://api.cohere.com/v2/embed + path: /v2/embed + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: embed-v4.0 + texts: + - Where do penguins live? + input_type: search_query + truncate: NONE + embedding_types: + - float + output_dimension: 256 +response: + status_code: 200 + headers: + content-type: application/json + body: + id: 416a76d9-313f-4c1e-bfa6-d1f7be36eeb2 + texts: + - Where do penguins live? + embeddings: + float: + - - 0.019325318 + - -0.013359112 + - 0.018287716 + - 0.051361248 + - -0.008754758 + - -0.081970476 + - 0.03190623 + - 0.028404325 + - -0.03164683 + - -0.076263666 + - 0.028144926 + - 0.06536886 + - 0.043579243 + - 0.09753449 + - -0.041504037 + - 0.082489274 + - 0.054214645 + - -0.029701326 + - -0.037353635 + - -0.14111373 + - -0.0030641651 + - 0.037613034 + - -0.2220466 + - -0.02101142 + - -0.050842445 + - 0.14837693 + - 0.010311159 + - 0.026718223 + - 0.028144926 + - -0.013164562 + - -0.073669665 + - -0.026199425 + - -0.1079105 + - -0.042801037 + - 0.059921455 + - -0.004134191 + - -0.121918105 + - -0.042541638 + - 0.124512106 + - -0.0033884153 + - 0.00058365054 + - 0.07418846 + - -0.05706805 + - 0.07107566 + - 0.07833887 + - 0.039169434 + - 0.098053284 + - 0.11984291 + - 0.15979055 + - -0.08145167 + - -0.004766479 + - 0.050064247 + - -0.009922059 + - 0.07833887 + - 0.07833887 + - 0.04383864 + - 0.13696332 + - -0.040466435 + - 0.03579723 + - 0.06407186 + - -0.0059337807 + - 0.005966205 + - -0.051880047 + - 0.09338408 + - 0.021789622 + - 0.048767246 + - -0.006938956 + - 0.06588766 + - -0.008073832 + - -0.099609695 + - -0.014850663 + - -0.010311159 + - -0.04435744 + - -0.016212514 + - -0.01134876 + - -0.01108936 + - 0.0035505407 + - -0.0077495817 + - 0.06536886 + - -0.03216563 + - 0.0024318772 + - 0.06277486 + - -0.019325318 + - 0.002204902 + - -0.04228224 + - 0.033462632 + - -0.081970476 + - 0.025161821 + - -0.02230842 + - -0.0061607556 + - 0.025421223 + - -0.070556864 + - 0.070556864 + - -0.098053284 + - 0.016990716 + - -0.04643264 + - 0.026069723 + - 0.02269752 + - -0.053436447 + - -0.0015239764 + - -0.082489274 + - -0.04228224 + - -0.03164683 + - 0.03281413 + - -0.030998327 + - 0.041504037 + - -0.09857209 + - 0.061996654 + - 0.0017752703 + - -0.03255473 + - -0.019844119 + - -0.18573056 + - 0.04306044 + - -0.03164683 + - 0.0050583044 + - 0.073150866 + - 0.0030641651 + - -0.028663727 + - 0.053177048 + - 0.037872434 + - 0.018287716 + - -0.19403137 + - 0.018676817 + - -0.060440257 + - -0.018158017 + - -0.027755825 + - 0.16394095 + - 0.051361248 + - -0.04772964 + - 0.15875295 + - 0.035278432 + - 0.04331984 + - 0.04435744 + - 0.07522607 + - -0.048767246 + - -0.0031938655 + - 0.115173705 + - -0.007846857 + - 0.035019033 + - -0.059402652 + - -0.0066147055 + - 0.016990716 + - 0.09027128 + - -0.011543311 + - 0.050323643 + - 0.014396713 + - -0.03268443 + - -0.08145167 + - 0.10842929 + - 0.007879282 + - -0.012451212 + - -0.04513564 + - -0.050583046 + - -0.08352687 + - -0.05525225 + - -0.02334602 + - 0.003210078 + - -0.025550924 + - -0.067444056 + - 0.023086622 + - -0.08975249 + - -0.06796286 + - -0.037872434 + - 0.18158016 + - 0.06951927 + - -0.106354095 + - -0.041244637 + - 0.041244637 + - 0.03294383 + - -0.079376474 + - 0.018936217 + - -0.05966205 + - 0.04669204 + - -0.09338408 + - 0.03424083 + - -0.012451212 + - 0.028404325 + - 0.04331984 + - -0.028015226 + - 0.06277486 + - 0.052139446 + - 0.042541638 + - 0.02165992 + - 0.019714417 + - -0.054733448 + - -0.01082996 + - -0.028015226 + - -0.05680865 + - 0.038131833 + - 0.08975249 + - 0.06407186 + - 0.038391236 + - 0.030609228 + - -0.024902424 + - 0.006549856 + - -0.08767728 + - -0.05499285 + - -0.016990716 + - 0.049545445 + - -0.053436447 + - -0.019973818 + - 0.051880047 + - -0.009468108 + - -0.030220129 + - 0.039947636 + - 0.039169434 + - -0.0019049704 + - 0.038910035 + - -0.005836505 + - 0.085602075 + - 0.019065917 + - -0.040207036 + - 0.005641955 + - 0.025680624 + - -0.028534025 + - -0.2095954 + - -0.027755825 + - -0.04539504 + - 0.07159446 + - -0.04772964 + - -0.016017964 + - -0.06536886 + - 0.051361248 + - 0.064590655 + - 0.029571628 + - -0.17327936 + - 0.018676817 + - 0.014980364 + - 0.046173245 + - -0.10583529 + - -0.0061607556 + - -0.1110233 + - 0.041244637 + - 0.038131833 + - 0.006679556 + - -0.03450023 + - 0.018676817 + - 0.04669204 + - -0.043579243 + - 0.07989527 + - 0.035019033 + - -0.115173705 + - -0.06510946 + - -0.010051759 + - 0.030479526 + - -0.004344954 + - 0.04331984 + - -0.030220129 + - -0.0036964535 + - 0.039947636 + - -0.09338408 + - 0.050842445 + - 0.016082816 + - 0.00554468 + - 0.05680865 + - -0.00290204 + meta: + api_version: + version: '2' + billed_units: + input_tokens: 6 + image_tokens: 0 + response_type: embeddings_by_type + body_bytes: !!binary | + eyJpZCI6IjQxNmE3NmQ5LTMxM2YtNGMxZS1iZmE2LWQxZjdiZTM2ZWViMiIsInRleHRzIjpbIldo + ZXJlIGRvIHBlbmd1aW5zIGxpdmU/Il0sImVtYmVkZGluZ3MiOnsiZmxvYXQiOltbMC4wMTkzMjUz + MTgsLTAuMDEzMzU5MTEyLDAuMDE4Mjg3NzE2LDAuMDUxMzYxMjQ4LC0wLjAwODc1NDc1OCwtMC4w + ODE5NzA0NzYsMC4wMzE5MDYyMywwLjAyODQwNDMyNSwtMC4wMzE2NDY4MywtMC4wNzYyNjM2NjYs + MC4wMjgxNDQ5MjYsMC4wNjUzNjg4NiwwLjA0MzU3OTI0MywwLjA5NzUzNDQ5LC0wLjA0MTUwNDAz + NywwLjA4MjQ4OTI3NCwwLjA1NDIxNDY0NSwtMC4wMjk3MDEzMjYsLTAuMDM3MzUzNjM1LC0wLjE0 + MTExMzczLC0wLjAwMzA2NDE2NTEsMC4wMzc2MTMwMzQsLTAuMjIyMDQ2NiwtMC4wMjEwMTE0Miwt + MC4wNTA4NDI0NDUsMC4xNDgzNzY5MywwLjAxMDMxMTE1OSwwLjAyNjcxODIyMywwLjAyODE0NDky + NiwtMC4wMTMxNjQ1NjIsLTAuMDczNjY5NjY1LC0wLjAyNjE5OTQyNSwtMC4xMDc5MTA1LC0wLjA0 + MjgwMTAzNywwLjA1OTkyMTQ1NSwtMC4wMDQxMzQxOTEsLTAuMTIxOTE4MTA1LC0wLjA0MjU0MTYz + OCwwLjEyNDUxMjEwNiwtMC4wMDMzODg0MTUzLDAuMDAwNTgzNjUwNTQsMC4wNzQxODg0NiwtMC4w + NTcwNjgwNSwwLjA3MTA3NTY2LDAuMDc4MzM4ODcsMC4wMzkxNjk0MzQsMC4wOTgwNTMyODQsMC4x + MTk4NDI5MSwwLjE1OTc5MDU1LC0wLjA4MTQ1MTY3LC0wLjAwNDc2NjQ3OSwwLjA1MDA2NDI0Nywt + MC4wMDk5MjIwNTksMC4wNzgzMzg4NywwLjA3ODMzODg3LDAuMDQzODM4NjQsMC4xMzY5NjMzMiwt + MC4wNDA0NjY0MzUsMC4wMzU3OTcyMywwLjA2NDA3MTg2LC0wLjAwNTkzMzc4MDcsMC4wMDU5NjYy + MDUsLTAuMDUxODgwMDQ3LDAuMDkzMzg0MDgsMC4wMjE3ODk2MjIsMC4wNDg3NjcyNDYsLTAuMDA2 + OTM4OTU2LDAuMDY1ODg3NjYsLTAuMDA4MDczODMyLC0wLjA5OTYwOTY5NSwtMC4wMTQ4NTA2NjMs + LTAuMDEwMzExMTU5LC0wLjA0NDM1NzQ0LC0wLjAxNjIxMjUxNCwtMC4wMTEzNDg3NiwtMC4wMTEw + ODkzNiwwLjAwMzU1MDU0MDcsLTAuMDA3NzQ5NTgxNywwLjA2NTM2ODg2LC0wLjAzMjE2NTYzLDAu + MDAyNDMxODc3MiwwLjA2Mjc3NDg2LC0wLjAxOTMyNTMxOCwwLjAwMjIwNDkwMiwtMC4wNDIyODIy + NCwwLjAzMzQ2MjYzMiwtMC4wODE5NzA0NzYsMC4wMjUxNjE4MjEsLTAuMDIyMzA4NDIsLTAuMDA2 + MTYwNzU1NiwwLjAyNTQyMTIyMywtMC4wNzA1NTY4NjQsMC4wNzA1NTY4NjQsLTAuMDk4MDUzMjg0 + LDAuMDE2OTkwNzE2LC0wLjA0NjQzMjY0LDAuMDI2MDY5NzIzLDAuMDIyNjk3NTIsLTAuMDUzNDM2 + NDQ3LC0wLjAwMTUyMzk3NjQsLTAuMDgyNDg5Mjc0LC0wLjA0MjI4MjI0LC0wLjAzMTY0NjgzLDAu + MDMyODE0MTMsLTAuMDMwOTk4MzI3LDAuMDQxNTA0MDM3LC0wLjA5ODU3MjA5LDAuMDYxOTk2NjU0 + LDAuMDAxNzc1MjcwMywtMC4wMzI1NTQ3MywtMC4wMTk4NDQxMTksLTAuMTg1NzMwNTYsMC4wNDMw + NjA0NCwtMC4wMzE2NDY4MywwLjAwNTA1ODMwNDQsMC4wNzMxNTA4NjYsMC4wMDMwNjQxNjUxLC0w + LjAyODY2MzcyNywwLjA1MzE3NzA0OCwwLjAzNzg3MjQzNCwwLjAxODI4NzcxNiwtMC4xOTQwMzEz + NywwLjAxODY3NjgxNywtMC4wNjA0NDAyNTcsLTAuMDE4MTU4MDE3LC0wLjAyNzc1NTgyNSwwLjE2 + Mzk0MDk1LDAuMDUxMzYxMjQ4LC0wLjA0NzcyOTY0LDAuMTU4NzUyOTUsMC4wMzUyNzg0MzIsMC4w + NDMzMTk4NCwwLjA0NDM1NzQ0LDAuMDc1MjI2MDcsLTAuMDQ4NzY3MjQ2LC0wLjAwMzE5Mzg2NTUs + MC4xMTUxNzM3MDUsLTAuMDA3ODQ2ODU3LDAuMDM1MDE5MDMzLC0wLjA1OTQwMjY1MiwtMC4wMDY2 + MTQ3MDU1LDAuMDE2OTkwNzE2LDAuMDkwMjcxMjgsLTAuMDExNTQzMzExLDAuMDUwMzIzNjQzLDAu + MDE0Mzk2NzEzLC0wLjAzMjY4NDQzLC0wLjA4MTQ1MTY3LDAuMTA4NDI5MjksMC4wMDc4NzkyODIs + LTAuMDEyNDUxMjEyLC0wLjA0NTEzNTY0LC0wLjA1MDU4MzA0NiwtMC4wODM1MjY4NywtMC4wNTUy + NTIyNSwtMC4wMjMzNDYwMiwwLjAwMzIxMDA3OCwtMC4wMjU1NTA5MjQsLTAuMDY3NDQ0MDU2LDAu + MDIzMDg2NjIyLC0wLjA4OTc1MjQ5LC0wLjA2Nzk2Mjg2LC0wLjAzNzg3MjQzNCwwLjE4MTU4MDE2 + LDAuMDY5NTE5MjcsLTAuMTA2MzU0MDk1LC0wLjA0MTI0NDYzNywwLjA0MTI0NDYzNywwLjAzMjk0 + MzgzLC0wLjA3OTM3NjQ3NCwwLjAxODkzNjIxNywtMC4wNTk2NjIwNSwwLjA0NjY5MjA0LC0wLjA5 + MzM4NDA4LDAuMDM0MjQwODMsLTAuMDEyNDUxMjEyLDAuMDI4NDA0MzI1LDAuMDQzMzE5ODQsLTAu + MDI4MDE1MjI2LDAuMDYyNzc0ODYsMC4wNTIxMzk0NDYsMC4wNDI1NDE2MzgsMC4wMjE2NTk5Miww + LjAxOTcxNDQxNywtMC4wNTQ3MzM0NDgsLTAuMDEwODI5OTYsLTAuMDI4MDE1MjI2LC0wLjA1Njgw + ODY1LDAuMDM4MTMxODMzLDAuMDg5NzUyNDksMC4wNjQwNzE4NiwwLjAzODM5MTIzNiwwLjAzMDYw + OTIyOCwtMC4wMjQ5MDI0MjQsMC4wMDY1NDk4NTYsLTAuMDg3Njc3MjgsLTAuMDU0OTkyODUsLTAu + MDE2OTkwNzE2LDAuMDQ5NTQ1NDQ1LC0wLjA1MzQzNjQ0NywtMC4wMTk5NzM4MTgsMC4wNTE4ODAw + NDcsLTAuMDA5NDY4MTA4LC0wLjAzMDIyMDEyOSwwLjAzOTk0NzYzNiwwLjAzOTE2OTQzNCwtMC4w + MDE5MDQ5NzA0LDAuMDM4OTEwMDM1LC0wLjAwNTgzNjUwNSwwLjA4NTYwMjA3NSwwLjAxOTA2NTkx + NywtMC4wNDAyMDcwMzYsMC4wMDU2NDE5NTUsMC4wMjU2ODA2MjQsLTAuMDI4NTM0MDI1LC0wLjIw + OTU5NTQsLTAuMDI3NzU1ODI1LC0wLjA0NTM5NTA0LDAuMDcxNTk0NDYsLTAuMDQ3NzI5NjQsLTAu + MDE2MDE3OTY0LC0wLjA2NTM2ODg2LDAuMDUxMzYxMjQ4LDAuMDY0NTkwNjU1LDAuMDI5NTcxNjI4 + LC0wLjE3MzI3OTM2LDAuMDE4Njc2ODE3LDAuMDE0OTgwMzY0LDAuMDQ2MTczMjQ1LC0wLjEwNTgz + NTI5LC0wLjAwNjE2MDc1NTYsLTAuMTExMDIzMywwLjA0MTI0NDYzNywwLjAzODEzMTgzMywwLjAw + NjY3OTU1NiwtMC4wMzQ1MDAyMywwLjAxODY3NjgxNywwLjA0NjY5MjA0LC0wLjA0MzU3OTI0Myww + LjA3OTg5NTI3LDAuMDM1MDE5MDMzLC0wLjExNTE3MzcwNSwtMC4wNjUxMDk0NiwtMC4wMTAwNTE3 + NTksMC4wMzA0Nzk1MjYsLTAuMDA0MzQ0OTU0LDAuMDQzMzE5ODQsLTAuMDMwMjIwMTI5LC0wLjAw + MzY5NjQ1MzUsMC4wMzk5NDc2MzYsLTAuMDkzMzg0MDgsMC4wNTA4NDI0NDUsMC4wMTYwODI4MTYs + MC4wMDU1NDQ2OCwwLjA1NjgwODY1LC0wLjAwMjkwMjA0XV19LCJtZXRhIjp7ImFwaV92ZXJzaW9u + Ijp7InZlcnNpb24iOiIyIn0sImJpbGxlZF91bml0cyI6eyJpbnB1dF90b2tlbnMiOjYsImltYWdl + X3Rva2VucyI6MH19LCJyZXNwb25zZV90eXBlIjoiZW1iZWRkaW5nc19ieV90eXBlIn0= diff --git a/tests/cassettes/embeddings/recorded/openai/batch-base64.yaml b/tests/cassettes/embeddings/recorded/openai/batch-base64.yaml new file mode 100644 index 0000000..299001a --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/batch-base64.yaml @@ -0,0 +1,42 @@ +timestamp: '2026-11-03T17:02:23.879069+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: + - A query about documents. + - Documents answer the query. + model: text-embedding-3-small + dimensions: 8 + encoding_format: base64 +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_40f872c2b50844d7890de7c1bf52c2a6 + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjoiQUlBZnZnQ2dNVDhBUUFFL0FDRFZ1d0Fnc1Q0QVlHRStBR0JQdmdDQU1UND0iLCJpbmRleCI6 + MH0seyJvYmplY3QiOiJlbWJlZGRpbmciLCJlbWJlZGRpbmciOiJBRUNOdmdDZ1F6OEF3SlUrQUtB + UFBnQUE3VDRBd0FJK0FBQjl2UUFnd3J3PSIsImluZGV4IjoxfV0sIm1vZGVsIjoidGV4dC1lbWJl + ZGRpbmctMy1zbWFsbCIsInVzYWdlIjp7InByb21wdF90b2tlbnMiOjEwLCJ0b3RhbF90b2tlbnMi + OjEwfX0= + body: + object: list + data: + - object: embedding + embedding: AIAfvgCgMT8AQAE/ACDVuwAgsT4AYGE+AGBPvgCAMT4= + index: 0 + - object: embedding + embedding: AECNvgCgQz8AwJU+AKAPPgAA7T4AwAI+AAB9vQAgwrw= + index: 1 + model: text-embedding-3-small + usage: + prompt_tokens: 10 + total_tokens: 10 diff --git a/tests/cassettes/embeddings/recorded/openai/batch-float.yaml b/tests/cassettes/embeddings/recorded/openai/batch-float.yaml new file mode 100644 index 0000000..bedbbb6 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/batch-float.yaml @@ -0,0 +1,61 @@ +timestamp: '2026-11-03T17:02:23.359257+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: + - A query about documents. + - Documents answer the query. + model: text-embedding-3-small + user: molto-public-embedding-fixture + dimensions: 8 + encoding_format: float +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_7ee7673b9b1d4371ac904b51d46c0eb2 + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjpbLTAuMTU1NzYxNzE4NzUsMC42OTM4NDc2NTYyNSwwLjUwNDg4MjgxMjUsLTAuMDA2NTA0MDU4 + ODM3ODkwNjI1LDAuMzQ1OTQ3MjY1NjI1LDAuMjIwMDkyNzczNDM3NSwtMC4yMDI1MTQ2NDg0Mzc1 + LDAuMTczMzM5ODQzNzVdLCJpbmRleCI6MH0seyJvYmplY3QiOiJlbWJlZGRpbmciLCJlbWJlZGRp + bmciOlstMC4yNzU4Nzg5MDYyNSwwLjc2NDE2MDE1NjI1LDAuMjkyNDgwNDY4NzUsMC4xNDAyNTg3 + ODkwNjI1LDAuNDYyODkwNjI1LDAuMTI3Njg1NTQ2ODc1LC0wLjA2MTc2NzU3ODEyNSwtMC4wMjM2 + OTY4OTk0MTQwNjI1XSwiaW5kZXgiOjF9XSwibW9kZWwiOiJ0ZXh0LWVtYmVkZGluZy0zLXNtYWxs + IiwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6MTAsInRvdGFsX3Rva2VucyI6MTB9fQ== + body: + object: list + data: + - object: embedding + embedding: + - -0.15576171875 + - 0.69384765625 + - 0.5048828125 + - -0.006504058837890625 + - 0.345947265625 + - 0.2200927734375 + - -0.2025146484375 + - 0.17333984375 + index: 0 + - object: embedding + embedding: + - -0.27587890625 + - 0.76416015625 + - 0.29248046875 + - 0.1402587890625 + - 0.462890625 + - 0.127685546875 + - -0.061767578125 + - -0.0236968994140625 + index: 1 + model: text-embedding-3-small + usage: + prompt_tokens: 10 + total_tokens: 10 diff --git a/tests/cassettes/embeddings/recorded/openai/default-dimensions.yaml b/tests/cassettes/embeddings/recorded/openai/default-dimensions.yaml new file mode 100644 index 0000000..6da6fb8 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/default-dimensions.yaml @@ -0,0 +1,177 @@ +timestamp: '2026-11-03T17:02:25.288312+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: A small public recording. + model: text-embedding-3-small + encoding_format: base64 +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_dd86721053a54b48810f09f2a8175143 + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjoiQUFEMFBBRGdJRHNBd0gwOEFDQVd2UUNBRXIwQVlMWTdBSUI2dkFBZ0Zqc0E0TXE4QUlBb3ZR + Q2dRVDBBQURDOUFJQ0d2QUFnbDd3QVlBbzhBT0J3dXdCZ0hEb0FBRVU3QUFBM1BBQ0E2YnNBb05J + N0FHQzd1d0NBbkRzQW9DSThBS0JTUFFEZ3NyMEFBTXE4QUdEV3ZBQmdxVHdBSU5ZN0FDRGNQQURn + V0wwQUFHKzhBQ0FldlFDQUo3MEFvQ3M5QUlBUVBRQ0FlTDBBb0JTN0FDREh2QURBV2p3QW9JZTlB + T0JZUEFBZ2Fid0E0RXc5QUdDSFBBQWdlTHdBQUFJOUFNQ2pQQUJnRUQwQWdLUzlBRURBUEFBQVhy + b0FnREU5QUtBZk93RGdoYnNBSUF3N0FDQTZQQUFBcUR3QXdHKzdBTUQ4UEFEQVpiMEFRRlU5QU9D + S1BBQWc5am9Bd0cwOEFHQjB2QURneGowQVFEdzlBR0NidWdBQUtEMEFJT1E3QUtCOHZRQ0FPYjBB + WU53OEFBQ05QUUNnSXp3QUFDUzhBQUQxdkFEQVI3d0FJQkk5QUdETlBBQ0FMTDBBb0pnOEFPQVRQ + QUNBSUx3QVlMeTlBSUNadkFBQUxqMEFZQUU5QU9BaXZRQ0FyN3dBZ0dXOEFHQTZQQUJBbVQwQVlB + aThBQ0RyT3dDQXNEc0FBQTA5QUdERFBBQ0FWYndBUUplOEFJQkx2QURBckwwQUFFMjhBTUN0T3dD + Z1pqd0FJRVE5QUNDSXZBQUFsenNBUUUyOUFHQWl2UUFnd3p3QVFNZzZBR0R5dXdEZ3k3d0F3R0U4 + QUdBbnZRRGd2YndBUU4yOEFPQ1l2QUJBenpzQVlPKzhBSUFkdlFDZ1JqMEFvRTg3QUlBdlBBQWd3 + endBNEFxOEFBQkp2UUNBU1QwQVlONjhBT0JsUEFBZ1NyMEFJT083QU9CdFBBQmdFendBb0phOEFH + QXJ2UUNBR3IwQWdFRTlBQUFNT3dCZ0NEMEF3QjQ2QUVDb3V3QmdKejBBSU5tOEFPQTFQUUJnTGow + QTRKZTdBRURaT3dCZ0hqMEF3QXE5QU9DN1BBQkE0THdBd0tXOEFLQ2xQQUFnRGpzQVlCZTlBQUNZ + UEFEZ3Nqb0FZSmM4QUdBcHZBQmdyVHdBNEtLOEFNQXhQUUJnbmp3QTRHZThBQ0M5dlFDQVNid0Fn + QXU5QUtCMFBBQ0ExenNBQUJNOUFJQ1N2UUJnU1R3QVlBUTlBS0NadlFCZ2FMMEE0Rk02QUlEM3V3 + QmdGVDBBd1BrOEFJQ0VQQUFncUxzQUFQNjhBQ0JLdXdCQUtqd0FJQlE5QU1EUU93QkFOTDBBNE1j + OEFPQmN2QUNnVVQwQVlPazdBRURxdkFBQVR6d0FnRUE4QUdBT3ZBREFwVHNBQUw2OEFFQk5QQUNn + YlR3QVlKbThBTUQ4dkFEZ0pMMEFJS0E4QUVEbVBBQkFiYnNBSUZhN0FBQnhQUUJBcGpzQTRBczhB + R0JZdkFCQW5yMEFBRms5QUVEOHVnQmdBendBb0pnN0FLQ0pQQUNncTd3QWdENDhBSUJKUFFEQURq + MEFRQzg3QU9CRFBRQWdxTHdBWUsyN0FFQ2lPd0NnVzcwQXdBKzhBRUFmdkFEZ3BiMEFZSUM4QU1D + RlBRQ2dVVDBBWUNTOUFNQ2F2QUJBNTd3QXdBbzdBRUNkUEFEZ0o3MEE0TDA3QUVBVHZBQ2dGendB + b0kyOEFBQ0p1d0JBZ3IwQVlGYzlBTUJIUEFEZ2R6d0FZSkk2QU1BWHZBQkFuYndBQU9XOEFHQkRP + d0Nnc1R3QUlCMDlBS0FKUEFBQUNUd0FvQ0M1QUVCanZRREFLYjBBWUFHOUFBRENPd0RnK0x3QXdH + UTlBT0F6UEFCZ2tqc0FBRUE5QUNBbHZRQ0FMN3dBWUVnN0FNRERQQUJBUzd3QTRNazdBRUJUUEFB + Z3RUc0FnQU04QUVDUVBRQWdCYnNBb0Y0OUFHQXV2UUFBWVQwQVFQNDdBRUM1UEFEZ2RqMEFvR1c2 + QUlDM1BBQmdGVHdBUU9TOEFFQUd2UUJBTkR3QVFBZTlBQUFDUFFCZ1hMd0FBRkE5QU9BVXZBQ2d1 + endBb0p5OEFPQVpQUUNBL0R3QVFPTzZBT0JzUEFBZ25id0FvSGs5QU9CZXZBQkFVN3dBUUdPNUFJ + QWd2UURBSUxzQW9DbzhBQUNHdlFEZzNMd0FRUG84QUlCYXZBRGd2RHNBUUZrN0FNRHBQQURnVXIw + QTRDSTlBQ0NYdlFBZ3dic0FBQW05QUNBNlBBQ2djRDBBSUljOEFPRFN2QUJnMFR3QVlFdThBQ0JH + dkFEZ0did0FJSzI1QUlEM1BBREFYajBBNEVXOUFHQXZPd0JBeGJ3QW9IczhBT0R2UEFEZ256d0E0 + TDY4QU9BVHZRQUFOVHNBd0xTOEFDQ1ZPd0JBSHowQUlFZThBQUM5T3dDQS9id0E0T2E4QUNBeHZB + Q2dYcjBBZ01JN0FHQlJQUURBY3J3QXdEYzhBQUFMdlFCQWNic0F3TTg4QUFBK3ZRQmc1enNBWUFz + OUFBQ2t2QUJnRXowQUlGMDlBTUMydlFDQVhyd0FnQ285QU9BSXZBQ2dBRDBBNEFJOEFJQ25QQUJB + Z0RzQVFBUzlBTUJEdkFBQVFMd0FZR0k5QU9Ba1BRQ0F2THNBWUNPOUFJQVp2QURBUTd3QTROTThB + TURPdkFCQWh6d0F3TGs4QUVCR1BRRGdUNzBBWUlhOEFNQ0VQQURBSTcwQVFORzhBRUQzdXdEQTFM + c0FJSmc4QUFETnV3REFjVDBBQUNpOEFNRHR1d0JncGpzQWdGKzhBTURKdkFEZ3lUd0FRQkM5QUFC + Y1BBQ2dWYjBBNENjOEFLRHlQQUFnbUx3QWdFbzhBQUIzdkFCZ2tqMEFJTEs4QUdEbk9RQWdyanNB + b0lrOEFBQjl1d0FBMzdvQTRIZzhBR0NUT3dCZ0JUb0FJQmk5QUdBeVBBQmdXenNBZ0o0OUFNQUt2 + UUJBNmJ3QW9HQThBSUE2dlFCQXBUb0FBRlM4QUtDeHZBQkFTanNBUUkwOEFFQUp2UUJBT3IwQVFB + bzlBQ0NrUEFCQVpqd0FnQkM5QUlBcXVnQkF3cnNBUUh1OEFFQ012QUJnUmJ3QTRLVTdBQUJidkFD + Z0pEd0FvTHU4QUtBb3ZBREFQN3dBUUNTOUFJQUd2UUJnSHowQUlEdTlBRUFkUFFBZ2J6d0F3TkM4 + QUFDMnZBQUE1VHNBZ0NzOEFBQVJ2QUFncGowQXdKbzZBTUFvdkFBQXFUd0FRTGs3QUlCZ3V3Q2d2 + RHdBd1BZOEFLRHF2QUFnY1R3QUFFMDlBTUFUdlFBQUxyMEFRTUE4QUVBb3ZRQ0FqYnNBUUR5OUFN + QmhQQUNnN2pzQVlLODVBT0NqUEFCQTJMc0FvSlU4QUtDSlBBQUF2RHNBb0FLN0FLQzJ2QUJBMTd3 + QVFCYzhBT0NXUEFEQXNid0FnTkM3QUNDSlBBQUFDN3dBWUJpOEFFRDNPd0FnUDd3QXdMeThBQ0FP + UFFDQXZid0FBQVk4QUVCV1BBQkEvandBNEYrOUFHQmt2QUNnR2owQW9JeThBRUFSUFFBZ2N6d0FB + Qkc5QUFDTnZBQkFvcndBZ0JHOUFJQWxQQUFnRmp3QUlONjdBT0RjT3dCQXlqd0E0TnU4QU1CMk93 + Q0FzN3dBQUlHOEFFQXFQQUFnQjd3QUlQRzhBR0FVUEFEZ083MEFJQmM4QUlDU1BBQmdLejBBQUZB + NkFHRDN2QURBK3J3QVlDazhBRUNDUEFDQW16d0FZQUU5QU9Cc3ZBQ0FtRHdBNEFHOUFNRFB2QUJn + azdrQVFFbzhBS0RFT3dEQWVyd0FRQzg3QU9CT3V3RGdBN3dBWUdPOEFPQTFQUUJnOUx3QVlOazhB + QUQ1UEFDQXdqd0F3RU04QUNEaU93Q0FwVHdBNEdNOEFBQ1pQQUFnL2p3QWdNTzdBRUIrdkFEQXVq + d0FJRzA4QUFDVXZBQmdyVHdBZ0NLOEFHQVB2QUNnTnJ3QVlNaThBSUQwdkFEZ01yd0E0TUU4QUVB + M1BRQ2cyTHNBNERnOEFHRHBQQURnRHp3QW9DdTlBTUNMUEFCZ0FMd0FvTHc4QUlDR3V3Q0FRTHNB + b05FOEFDQ0RQQUJBOXJzQWdFbzlBQ0EvUEFEZzBid0FnT3M4QUlEWlBBQ0F5RHNBb0pROEFHQWJ2 + UUNBTHIwQTRMVzhBR0N0UEFDZ1BEMEFJTTA0QUlCWFBBQ2c3endBQU04N0FBQ3N2QURBQnIwQVlD + NjhBQ0J4UEFEZ2VUd0FvUEU4QUtBQnZBQmd6YndBQUJ1OEFDQzJ1d0FBekx3QUFMdzZBR0IzdXdB + Z1REd0F3SGk3QUtBdXV3QWd4THdBQU1vOEFBRFh2QUFBRFQwQUlDYTlBSUJtdkFCQVhMd0F3TFE4 + QU9BL3ZRREFMRDBBWU1jOEFHQUdQQUFnUnp3QW9NNjhBSUR3dkFCZ01Ud0FRRlE2QUtDYnZBQ2dV + VDBBQU8rOEFLQ1FQUUFnWXJ3QUFIazlBRUNydkFCZ2h6d0FvSm83QU9CME9nQ2dHYjBBUUVBN0FD + QzV2QUJnbmJzQWdIUTdBQ0NKdkFDQXRqc0FnQnc4QU9BNnV3QmdHNzBBZ0tvOEFPQ0ZPd0FBTHow + QUlJUThBRUFXT2dDQXJ6c0F3UGM4QU9BVnZRQWcrTHNBUUh1OEFDRFB1d0JBazdzQWdDZTlBQ0FU + UFFEQUFUd0FvQWE3QUFBdk93QUExN29BZ1BHOEFLQ3V2QUNnaTdvQUFIODhBS0FpdXdBZzVid0FZ + QlM4QU9BZ3ZRREFNcndBb0FrOUFFRGFQQUJBd2p3QWdNRTVBQUNIdkFCZ05Uc0FZSWU1QUNDNFBB + QUFyYndBd0VFOEFDQ2lQQUNBdnJ3QWdJZzhBQ0RCdlFBQVlyd0FvS281QUNDT3ZBQUFNem9Bd0FX + OEFNRFBQQURBQmJ3QW9NSzhBQUFKUEFCQTZid0FBSTQ4QUdDbnZBREFtN3dBNEJnOUFDQ1l1d0NB + b2J3QW9OSzdBTURxT2dEZ05Eb0F3S3c4QU9ETnZBQ2dFRDBBWUpnOEFDQTR2UUFBUDd3QWdDczhB + Q0E2UEFEZ3NEd0FJSnk4QUFDSlBBREF1cndBSUFzOUFBQ29Pd0RBZkRzQXdIYTZBQ0RpUEFCQXFM + MEFnRHc5QUlBaHZBQUExN3dBUU9ROEFDQ3pQQUJBbHp3QXdBczlBT0NST3dBQWJMd0FJTmk3QUdE + R1BBREFQTDBBNEFTOUFPRFR2QUFnakR3QWdGRzlBT0RwUEFDZ1RUMEFnT204QUNDQ3ZBQkFaenNB + UU5pOEFLQjdQQURnYTd3QTRKRzdBR0R6T3dBQU56MEFZSFc3QUVBMXZBQ0FUYndBWURPOUFHRHBP + d0RBQTcwQVFOeTdBR0JSdXdBQWJid0FvQTA5QUFCRVBRRGdhem9BSUJPN0FBQ2lPd0JBOUR3QVlN + TzdBS0NzUEFBZzZic0FZS1k4QUFCS3ZBREF6cndBb0Z3OEFDQ2h2QUJBbkRzQWdFVzlBR0MrT1FE + QXQ3d0E0THU2QUtCQ1BBQ2dqcndBSUZ3OEFLQzh1Z0RnUXJ3QW9JKzlBSUJGUEFCQUl6MEE0UHU4 + QU1DRk93Q0FtYnNBWUY0OEFDRDlPUUJBSUwwQXdCMjlBS0JXdkFEZ0I3d0FRSVU1QUNCbnZRREFh + andBb0F3OUFBQUd2UUNBMWp3QTRETTlBQ0FPUEFCZ2l6d0FBRDI4QUFDQlBBQ0FtVHdBb040OEFJ + REhQQURBUHIwQVlDdzlBR0ExdkFBZ1hyMEFJRmM5QU1CRVBRQWdBRDBBNE5vOEFBQjB2QUFBQnJ3 + QVFEUzlBQ0F4UEFCZzZMd0FvQXM4QUVBOHV3REFqcndBSUwyOEFBQ092QUJBSmowQXdITThBS0NE + UEFCQUpEMEFvS2c4QUNBWVBBQ0FNejBBd013OEFNQzBQQURBQ0wwQVFQVTdBTUFEUFFBZ0N6d0Fn + Qks5QUdBN3ZRQ0FvcnNBb0R5OUFNQVlQQUNBNlR3QVFCczhBQ0NmdkFEZzJMc0E0QU05QUFDOXV3 + QmdnN3dBZ1BTOEFDQUxQQURBeVRzQTRBYTlBRUJ2dkFBQXpqc0FRTDA4QU9DRlBBQ0FxRHdBWUdJ + N0FDRGtPZ0RnaHp3QTRJcTZBS0JFdXdCQWc3d0FBS0c4QUlCZU93Q2dFN3dBWUwwN0FFQkJ1d0JB + a3J3QVFMbzdBTUFYUEFCZ0o3c0FvS2s1QUFCbnZRQWdRejBBQUQ2OEFDQ1h2QUNnSERvQVlESThB + T0FmUFFBQUY3MEFZQ3U5QU9CcXVnQUEwandBZ0hLN0FFQ1JQQUNnc2p3QVlBODhBQ0NEdlFDZ0Zy + b0E0Q084QUlBSlBRQWdTTDBBNEhDN0FDQ3lQQUJBRkQwQUFLKzZBQURZdkFBZ2dib0FvTEc4QU1E + VnV3QWd4THNBSUMrOEFJRDJPd0JnOXpvQW9GUTdBTUJ1UEFDZzhEd0FRTFc4QUFDSVBRQkFsVHdB + QU1hOEFNQk9Pd0JBQnowQVFLazdBT0RZT3dCQTVUc0FBQ0s5QUlER3ZBQmdWN3dBNEY0N0FPQkJQ + QURncDdzQUlBbThBTUIrT3dBQU03d0F3Sms4QUNBTHZRRGdERDBBUUtxOEFJQytPd0NnRHJ3QVlD + SzlBQURUdkFCZ1Rqd0FBQ1U4QUlCeVBRQWdWcjBBd0FFN0FFQlZ2QUJnTEQwQTRMTzhBRUFRdkFB + QWpUd0FJTnc2QUNBU3ZBQUFZTDBBQU0yOEFLRFRPd0FnWmJ3QTRDZzlBTUFlUFFCQUg3b0E0Qnk2 + QUdBOHV3REFCRDBBNEEwOEFFRDFQQUFnNnpzQTRDMjhBQ0RIdWdEZ3c3c0FBS1k3QUlEa1BBRGdP + RDBBUUp5OEFBQ2x2QUJBRnp3QTRJSzdBT0RoUEFBQUJ6d0F3R204QUtEbXZBRGdXRHdBd0lTOEFH + QmdQQUNBNEx3QVFMRzhBQUFhUFFEZ0Vyd0F3SlU4QUNEQVBBQ0EyandBNEM0OEFDQVR2QUJnVTdz + QVlLNjZBQUFIdkFDQWo3d0FRTHk4QUNDMXZBREFQYndBNEcyOEFNRFdQQURBcTdzQVlBcTlBSUF0 + UEFBQVE3MEE0TE83QUtEelBBRGdnTHdBb0FxN0FDRE9QQUJBQWJ3QVlDQTlBR0RKdkFDQUlMd0Fv + RU84QUtEZHVnQWdEendBSUprOEFJRDBQQUJBTjcwQW9DazhBS0FndkFCQTlqc0E0QmM4QUNDZFBB + QmdqYndBb0E4OEFJQ2FPd0JnVEwwQXdBbThBTUR4UEFBZ0did0FBRkk3QUNBbHZRQmc2RHdBQUlT + NUFNQTRPd0JnQ2p3QUlMSTVBT0I4dkFCQVpUd0F3RXM4QUNDaHV3REE0YndBWU5VOEFNQkxQQUJB + REwwQXdJQzhBSUNkUEFDQUZyMEFvQ1E5QUlDTnZBQWdMTDBBNE1rOEFDQ0pPd0RnVVR3QTRDSzhB + R0JUdXdBQUtqMEFZT2k3QUdCaVBBQWdCajBBWUNDOEFHRHpPd0FBbDdzQUFOQThBQUQ5dkFDZzBE + d0E0R1U4QU9DRHZBQ2dsYnNBUURPOUFFQzd2QUJnUkR3QVFOTTZBQUMrUEFEZ29Mc0FvQWM4QUVE + VHV3Q2dScndBWUcwNkFNQ2R2QUNBcFR3QW9DczdBSUI3UEFBQVhEd0FZSVc4QUtBZVBBQkFDcnNB + QUJDOUFLQ3FQQUFneHp3QWdGNjZBQUJ6UEFDZ3Ryd0FJQkk5QU1BU1BBREFBRDBBUUJzOUFDQUFQ + QURnMnJ3QXdMbzhBSUEwdlFBQTFMd0FBTUU3QU9CaU93REFXTHdBSUl1OEFDRFFPd0FncUR3QXdH + KzZBT0RzdXdEZ2ZEc0FBQ0c4QU1EUk93QUF6THdBUUpjOEFLQVJQQUFBeFRzQW9GRTlBQUJPUEFB + QUxEMEE0RFU5QUFDU3ZBQWdGVHdBNEhROEFNQkNQQUJnU3IwQUlHODhBTUJCUEFDQTU3d0FZTEs4 + QUVEcFBBQkFwYm9BUVBlNkFJQkd2QUFBQkQ0QW9PRzdBQUFFdkFEZ01yc0FJSnc3QUFDalBBQWd3 + RHNBWUhTOEFHQVJQQUFnTHowQW9EbzhBQUFMUFFDQU9yd0FvR083QUlBM3VRQ2ducnNBQUdZOEFD + Q1N2QURBbWp3QTRKKzdBQUI0UEFEQUVyMEFvQ283QUNDaHZBQmdIYndBb01POEFBQXV2QURBTnJ3 + QVlEQzhBQ0FNUEFEQSt6d0FvSFM3QUFDaFBBQWdvN3NBb0wrOEFNQ3BPZ0JBcnJ3QVlPNDdBTUMz + dkFCZ0pyd0FvT3U3QUFBWnZBQmdLVHdBQUU2OEFNQ0xPd0JnQVRzQWdMaThBQ0NDdkFCZ2lyd0E0 + Rmk3QUdCeVBBQmdsVHdBNE1hOEFHRHdQQUJBMlRzQW9BRzdBQUJDdkFEZ3hqc0FvQTQ4QUlDZE9n + Q2c1THNBZ0xBN0FPQTFQUURnemJ3QUlOSThBSUFWdlFBZ1hib0FvTEc3QUdER1BBQ0FWamtBZ0lB + OEFNQlVQUUFBZ1R3QVFCdTdBTUJ3T3dDZ05yd0FJRFc4QUtCQnZBRGdXendBd0ZHN0FFRGN1Z0RB + M3pvQVFQVThBTURXdWdDQXo3d0F3TDg3QU1EQXV3REFFcndBUUNpN0FJRHVPUUFnaTd3QUFINDhB + TUNKdkFEQVM3b0FvTTQ4QUNDYlBBQ2d5RHNBSUNHOEFJQXd2UUFBcWJ3QUlMMDhBQUFCdkFBZ2Vy + d0E0Tlc3QUVCL3V3Q2dXRHNBNEpjN0FFRGN2QURnUnJ3QWdLWTdBT0RSdkFCZ25id0FZUFM3QU9E + UlBBQ0EwendBWURlOUFFQ3ZPd0FnUWp3QXdMRThBQ0NxdkFDZ0ZUMEFRSlU4QU9CRlBBQkEvcnNB + QUtFOEFDQUNQUURnUXowQXdJczdBQUREUEFEZy9yc0FJQjQ5QUdCTnZRQUFCejBBb0ZTN0FFQ0NQ + QUJnN2p3QUlGcThBSUQ0dWdCZzhEd0FnSWk4QUVDZk93REFlYndBQUVXOEFJQUJ2QUJnV0x3QUlI + azhBRURqT3dEQTlMc0FnQmc4QUNCQnZRQmdHN3dBZ0pHOEFPQUp2UUFnc1R3QXdFYzhBSUJpUEFC + QVdEd0FBRVM2QUdBdU93QWcxendBNEgyOEFDQk51d0FnQnJzQUlMazhBQ0RMT3dCQUFUd0E0S3E4 + QUFBOXZBQkFWamdBZ0pFN0FLQUtQQURBNkRzQWdQcThBT0J1dkFDQVdqc0FJRUk4QUFEUnZBQUF4 + anNBSUUyOUFLQ3p1d0JnOXJ3QXdFNjhBT0NDUEFDZ016MEFBS0U4QUNCQlBBRGd1THdBd0ZDOEFD + QVFQQURBYUx3QVFHQThBT0JhUEFCQTdMc0FZREk3QU9DQ1BBQkFiN3dBZ0VTOEFDQUdQUURnVTd3 + QUlGNjhBQURodkFDQUVMc0E0SEU3QUFEQlBBQ0FGejBBWUNpN0FDREF1d0FBbHpzQW9EazdBQUFq + T3dBQWxEd0E0RnU4QUtBQ1BBQ2dKRHdBNERPOEFNQ3RQQURnQ3J3QXdMRTdBSURydkFDQWpUd0Fn + TVE4QUlBWVBBQ0FJVHdBd0RLOUFHQlB2QUFndkxrQUFBTThBQURuT3dEQXdqd0FRTmM1QUtDTXZB + RGc3N3NBZ0NJOEFFQUNQQUJnb3J3QWdDMDhBT0NMT3dEZytid0FvQlM5QU9CMnV3QmdCYndBWU8y + N0FJQi9PZ0NBYjd3QVlMMDZBS0Q3T1FBZ3U3b0E0R0U4QUdBZ1BBQ0FXemdBNE8rOEFJQmdPUUNB + UlRzQUlOaThBS0FxdkFEQWdid0FZQzY4QUlBSXZRQWduRHdBNERpOEFFQS92QURBRXIwQW9DNjdB + R0RsUEFDZ0diMEFnT0U4QU1DbU93Q2dDYm9BZ00rN0FHQUh2QUJBQnJ3QXdQNjdBR0MzUEFCQXhq + d0FvTk84QU1Bc1BBQ0FnYnNBSUwyN0FJQUV1d0JndWJ3QW9JZzhBR0RLdkFBZ05id0F3REk5QU9B + RFBBQmdvRHdBd1BFN0FNQSt2QUFBeTdzQTRQSTdBRUR2dkFEZ216d0FJQWM4QUFBRlBBREFoenNB + WVBFOEFHQUt2UURnaFRzQWdQUTdBRURQdXdDZ3ZMd0FBQkc3QUFBSHZBQkFzcnNBSUtPOEFHQkZ2 + QUFnbExzQXdDczlBT0NadXdCQVRqd0FRTnU1QUdBL3V3RGdFamtBUVBjN0FJQmFQQUFneER3QTRP + WTdBR0F2dXdDQUFyMEFnSlU3QUdDOXV3QkFrandBd05HNkFHQnpQQUNnY1R3QW9DazhBSUJ5dkFC + Z2w3c0F3RU04QUNBT1BBREFHajBBZ0VJOEFNQ1JQQUNnNmJ3QTRBNDhBQUE3dkFBZ0piMEF3S284 + QUdCZXZBQUE4RG9BZ0crN0FBQ2dQQURBY3p3QXdJbThBQUFOUEFCQUdUMEF3S1M4QUtBcXV3QUFQ + ejBBWUIrOEFBQVZ2UUFndlR3QVFCcTlBRUR5UEFBQXk3d0E0Tks2QUNDaFBBQ0FSN3dBd0ZLOEFN + QVd2UUJnRWp3QVFNUzZBRURVT3dEZ2pyd0FvRkc5QU1DR1BBQWc2THNBWUF3OUFFQ29QQUFBR0Qw + QW9CaTlBQUFzT3dBQTlyd0FBTCs4QUNDS1BBREE1THNBQUcwOEFPQUpQQURBQnprQVFGdzhBQ0RZ + UEFDZ0Fqd0FvQU84QUlCbE9nQWdqRHdBZ0N5OUFBQVp2UUNnUWJ3QVlEYTgiLCJpbmRleCI6MH1d + LCJtb2RlbCI6InRleHQtZW1iZWRkaW5nLTMtc21hbGwiLCJ1c2FnZSI6eyJwcm9tcHRfdG9rZW5z + Ijo1LCJ0b3RhbF90b2tlbnMiOjV9fQ== + body: + object: list + data: + - object: embedding + embedding: AAD0PADgIDsAwH08ACAWvQCAEr0AYLY7AIB6vAAgFjsA4Mq8AIAovQCgQT0AADC9AICGvAAgl7wAYAo8AOBwuwBgHDoAAEU7AAA3PACA6bsAoNI7AGC7uwCAnDsAoCI8AKBSPQDgsr0AAMq8AGDWvABgqTwAINY7ACDcPADgWL0AAG+8ACAevQCAJ70AoCs9AIAQPQCAeL0AoBS7ACDHvADAWjwAoIe9AOBYPAAgabwA4Ew9AGCHPAAgeLwAAAI9AMCjPABgED0AgKS9AEDAPAAAXroAgDE9AKAfOwDghbsAIAw7ACA6PAAAqDwAwG+7AMD8PADAZb0AQFU9AOCKPAAg9joAwG08AGB0vADgxj0AQDw9AGCbugAAKD0AIOQ7AKB8vQCAOb0AYNw8AACNPQCgIzwAACS8AAD1vADAR7wAIBI9AGDNPACALL0AoJg8AOATPACAILwAYLy9AICZvAAALj0AYAE9AOAivQCAr7wAgGW8AGA6PABAmT0AYAi8ACDrOwCAsDsAAA09AGDDPACAVbwAQJe8AIBLvADArL0AAE28AMCtOwCgZjwAIEQ9ACCIvAAAlzsAQE29AGAivQAgwzwAQMg6AGDyuwDgy7wAwGE8AGAnvQDgvbwAQN28AOCYvABAzzsAYO+8AIAdvQCgRj0AoE87AIAvPAAgwzwA4Aq8AABJvQCAST0AYN68AOBlPAAgSr0AIOO7AOBtPABgEzwAoJa8AGArvQCAGr0AgEE9AAAMOwBgCD0AwB46AECouwBgJz0AINm8AOA1PQBgLj0A4Je7AEDZOwBgHj0AwAq9AOC7PABA4LwAwKW8AKClPAAgDjsAYBe9AACYPADgsjoAYJc8AGApvABgrTwA4KK8AMAxPQBgnjwA4Ge8ACC9vQCASbwAgAu9AKB0PACA1zsAABM9AICSvQBgSTwAYAQ9AKCZvQBgaL0A4FM6AID3uwBgFT0AwPk8AICEPAAgqLsAAP68ACBKuwBAKjwAIBQ9AMDQOwBANL0A4Mc8AOBcvACgUT0AYOk7AEDqvAAATzwAgEA8AGAOvADApTsAAL68AEBNPACgbTwAYJm8AMD8vADgJL0AIKA8AEDmPABAbbsAIFa7AABxPQBApjsA4As8AGBYvABAnr0AAFk9AED8ugBgAzwAoJg7AKCJPACgq7wAgD48AIBJPQDADj0AQC87AOBDPQAgqLwAYK27AECiOwCgW70AwA+8AEAfvADgpb0AYIC8AMCFPQCgUT0AYCS9AMCavABA57wAwAo7AECdPADgJ70A4L07AEATvACgFzwAoI28AACJuwBAgr0AYFc9AMBHPADgdzwAYJI6AMAXvABAnbwAAOW8AGBDOwCgsTwAIB09AKAJPAAACTwAoCC5AEBjvQDAKb0AYAG9AADCOwDg+LwAwGQ9AOAzPABgkjsAAEA9ACAlvQCAL7wAYEg7AMDDPABAS7wA4Mk7AEBTPAAgtTsAgAM8AECQPQAgBbsAoF49AGAuvQAAYT0AQP47AEC5PADgdj0AoGW6AIC3PABgFTwAQOS8AEAGvQBANDwAQAe9AAACPQBgXLwAAFA9AOAUvACguzwAoJy8AOAZPQCA/DwAQOO6AOBsPAAgnbwAoHk9AOBevABAU7wAQGO5AIAgvQDAILsAoCo8AACGvQDg3LwAQPo8AIBavADgvDsAQFk7AMDpPADgUr0A4CI9ACCXvQAgwbsAAAm9ACA6PACgcD0AIIc8AODSvABg0TwAYEu8ACBGvADgGbwAIK25AID3PADAXj0A4EW9AGAvOwBAxbwAoHs8AODvPADgnzwA4L68AOATvQAANTsAwLS8ACCVOwBAHz0AIEe8AAC9OwCA/bwA4Oa8ACAxvACgXr0AgMI7AGBRPQDAcrwAwDc8AAALvQBAcbsAwM88AAA+vQBg5zsAYAs9AACkvABgEz0AIF09AMC2vQCAXrwAgCo9AOAIvACgAD0A4AI8AICnPABAgDsAQAS9AMBDvAAAQLwAYGI9AOAkPQCAvLsAYCO9AIAZvADAQ7wA4NM8AMDOvABAhzwAwLk8AEBGPQDgT70AYIa8AMCEPADAI70AQNG8AED3uwDA1LsAIJg8AADNuwDAcT0AACi8AMDtuwBgpjsAgF+8AMDJvADgyTwAQBC9AABcPACgVb0A4Cc8AKDyPAAgmLwAgEo8AAB3vABgkj0AILK8AGDnOQAgrjsAoIk8AAB9uwAA37oA4Hg8AGCTOwBgBToAIBi9AGAyPABgWzsAgJ49AMAKvQBA6bwAoGA8AIA6vQBApToAAFS8AKCxvABASjsAQI08AEAJvQBAOr0AQAo9ACCkPABAZjwAgBC9AIAqugBAwrsAQHu8AECMvABgRbwA4KU7AABbvACgJDwAoLu8AKAovADAP7wAQCS9AIAGvQBgHz0AIDu9AEAdPQAgbzwAwNC8AAC2vAAA5TsAgCs8AAARvAAgpj0AwJo6AMAovAAAqTwAQLk7AIBguwCgvDwAwPY8AKDqvAAgcTwAAE09AMATvQAALr0AQMA8AEAovQCAjbsAQDy9AMBhPACg7jsAYK85AOCjPABA2LsAoJU8AKCJPAAAvDsAoAK7AKC2vABA17wAQBc8AOCWPADAsbwAgNC7ACCJPAAAC7wAYBi8AED3OwAgP7wAwLy8ACAOPQCAvbwAAAY8AEBWPABA/jwA4F+9AGBkvACgGj0AoIy8AEARPQAgczwAABG9AACNvABAorwAgBG9AIAlPAAgFjwAIN67AODcOwBAyjwA4Nu8AMB2OwCAs7wAAIG8AEAqPAAgB7wAIPG8AGAUPADgO70AIBc8AICSPABgKz0AAFA6AGD3vADA+rwAYCk8AECCPACAmzwAYAE9AOBsvACAmDwA4AG9AMDPvABgk7kAQEo8AKDEOwDAerwAQC87AOBOuwDgA7wAYGO8AOA1PQBg9LwAYNk8AAD5PACAwjwAwEM8ACDiOwCApTwA4GM8AACZPAAg/jwAgMO7AEB+vADAujwAIG08AACUvABgrTwAgCK8AGAPvACgNrwAYMi8AID0vADgMrwA4ME8AEA3PQCg2LsA4Dg8AGDpPADgDzwAoCu9AMCLPABgALwAoLw8AICGuwCAQLsAoNE8ACCDPABA9rsAgEo9ACA/PADg0bwAgOs8AIDZPACAyDsAoJQ8AGAbvQCALr0A4LW8AGCtPACgPD0AIM04AIBXPACg7zwAAM87AACsvADABr0AYC68ACBxPADgeTwAoPE8AKABvABgzbwAABu8ACC2uwAAzLwAALw6AGB3uwAgTDwAwHi7AKAuuwAgxLwAAMo8AADXvAAADT0AICa9AIBmvABAXLwAwLQ8AOA/vQDALD0AYMc8AGAGPAAgRzwAoM68AIDwvABgMTwAQFQ6AKCbvACgUT0AAO+8AKCQPQAgYrwAAHk9AECrvABghzwAoJo7AOB0OgCgGb0AQEA7ACC5vABgnbsAgHQ7ACCJvACAtjsAgBw8AOA6uwBgG70AgKo8AOCFOwAALz0AIIQ8AEAWOgCArzsAwPc8AOAVvQAg+LsAQHu8ACDPuwBAk7sAgCe9ACATPQDAATwAoAa7AAAvOwAA17oAgPG8AKCuvACgi7oAAH88AKAiuwAg5bwAYBS8AOAgvQDAMrwAoAk9AEDaPABAwjwAgME5AACHvABgNTsAYIe5ACC4PAAArbwAwEE8ACCiPACAvrwAgIg8ACDBvQAAYrwAoKo5ACCOvAAAMzoAwAW8AMDPPADABbwAoMK8AAAJPABA6bwAAI48AGCnvADAm7wA4Bg9ACCYuwCAobwAoNK7AMDqOgDgNDoAwKw8AODNvACgED0AYJg8ACA4vQAAP7wAgCs8ACA6PADgsDwAIJy8AACJPADAurwAIAs9AACoOwDAfDsAwHa6ACDiPABAqL0AgDw9AIAhvAAA17wAQOQ8ACCzPABAlzwAwAs9AOCROwAAbLwAINi7AGDGPADAPL0A4AS9AODTvAAgjDwAgFG9AODpPACgTT0AgOm8ACCCvABAZzsAQNi8AKB7PADga7wA4JG7AGDzOwAANz0AYHW7AEA1vACATbwAYDO9AGDpOwDAA70AQNy7AGBRuwAAbbwAoA09AABEPQDgazoAIBO7AACiOwBA9DwAYMO7AKCsPAAg6bsAYKY8AABKvADAzrwAoFw8ACChvABAnDsAgEW9AGC+OQDAt7wA4Lu6AKBCPACgjrwAIFw8AKC8ugDgQrwAoI+9AIBFPABAIz0A4Pu8AMCFOwCAmbsAYF48ACD9OQBAIL0AwB29AKBWvADgB7wAQIU5ACBnvQDAajwAoAw9AAAGvQCA1jwA4DM9ACAOPABgizwAAD28AACBPACAmTwAoN48AIDHPADAPr0AYCw9AGA1vAAgXr0AIFc9AMBEPQAgAD0A4No8AAB0vAAABrwAQDS9ACAxPABg6LwAoAs8AEA8uwDAjrwAIL28AACOvABAJj0AwHM8AKCDPABAJD0AoKg8ACAYPACAMz0AwMw8AMC0PADACL0AQPU7AMADPQAgCzwAgBK9AGA7vQCAorsAoDy9AMAYPACA6TwAQBs8ACCfvADg2LsA4AM9AAC9uwBgg7wAgPS8ACALPADAyTsA4Aa9AEBvvAAAzjsAQL08AOCFPACAqDwAYGI7ACDkOgDghzwA4Iq6AKBEuwBAg7wAAKG8AIBeOwCgE7wAYL07AEBBuwBAkrwAQLo7AMAXPABgJ7sAoKk5AABnvQAgQz0AAD68ACCXvACgHDoAYDI8AOAfPQAAF70AYCu9AOBqugAA0jwAgHK7AECRPACgsjwAYA88ACCDvQCgFroA4CO8AIAJPQAgSL0A4HC7ACCyPABAFD0AAK+6AADYvAAggboAoLG8AMDVuwAgxLsAIC+8AID2OwBg9zoAoFQ7AMBuPACg8DwAQLW8AACIPQBAlTwAAMa8AMBOOwBABz0AQKk7AODYOwBA5TsAACK9AIDGvABgV7wA4F47AOBBPADgp7sAIAm8AMB+OwAAM7wAwJk8ACALvQDgDD0AQKq8AIC+OwCgDrwAYCK9AADTvABgTjwAACU8AIByPQAgVr0AwAE7AEBVvABgLD0A4LO8AEAQvAAAjTwAINw6ACASvAAAYL0AAM28AKDTOwAgZbwA4Cg9AMAePQBAH7oA4By6AGA8uwDABD0A4A08AED1PAAg6zsA4C28ACDHugDgw7sAAKY7AIDkPADgOD0AQJy8AAClvABAFzwA4IK7AODhPAAABzwAwGm8AKDmvADgWDwAwIS8AGBgPACA4LwAQLG8AAAaPQDgErwAwJU8ACDAPACA2jwA4C48ACATvABgU7sAYK66AAAHvACAj7wAQLy8ACC1vADAPbwA4G28AMDWPADAq7sAYAq9AIAtPAAAQ70A4LO7AKDzPADggLwAoAq7ACDOPABAAbwAYCA9AGDJvACAILwAoEO8AKDdugAgDzwAIJk8AID0PABAN70AoCk8AKAgvABA9jsA4Bc8ACCdPABgjbwAoA88AICaOwBgTL0AwAm8AMDxPAAgGbwAAFI7ACAlvQBg6DwAAIS5AMA4OwBgCjwAILI5AOB8vABAZTwAwEs8ACChuwDA4bwAYNU8AMBLPABADL0AwIC8AICdPACAFr0AoCQ9AICNvAAgLL0A4Mk8ACCJOwDgUTwA4CK8AGBTuwAAKj0AYOi7AGBiPAAgBj0AYCC8AGDzOwAAl7sAANA8AAD9vACg0DwA4GU8AOCDvACglbsAQDO9AEC7vABgRDwAQNM6AAC+PADgoLsAoAc8AEDTuwCgRrwAYG06AMCdvACApTwAoCs7AIB7PAAAXDwAYIW8AKAePABACrsAABC9AKCqPAAgxzwAgF66AABzPACgtrwAIBI9AMASPADAAD0AQBs9ACAAPADg2rwAwLo8AIA0vQAA1LwAAME7AOBiOwDAWLwAIIu8ACDQOwAgqDwAwG+6AODsuwDgfDsAACG8AMDROwAAzLwAQJc8AKARPAAAxTsAoFE9AABOPAAALD0A4DU9AACSvAAgFTwA4HQ8AMBCPABgSr0AIG88AMBBPACA57wAYLK8AEDpPABApboAQPe6AIBGvAAABD4AoOG7AAAEvADgMrsAIJw7AACjPAAgwDsAYHS8AGARPAAgLz0AoDo8AAALPQCAOrwAoGO7AIA3uQCgnrsAAGY8ACCSvADAmjwA4J+7AAB4PADAEr0AoCo7ACChvABgHbwAoMO8AAAuvADANrwAYDC8ACAMPADA+zwAoHS7AAChPAAgo7sAoL+8AMCpOgBArrwAYO47AMC3vABgJrwAoOu7AAAZvABgKTwAAE68AMCLOwBgATsAgLi8ACCCvABgirwA4Fi7AGByPABglTwA4Ma8AGDwPABA2TsAoAG7AABCvADgxjsAoA48AICdOgCg5LsAgLA7AOA1PQDgzbwAINI8AIAVvQAgXboAoLG7AGDGPACAVjkAgIA8AMBUPQAAgTwAQBu7AMBwOwCgNrwAIDW8AKBBvADgWzwAwFG7AEDcugDA3zoAQPU8AMDWugCAz7wAwL87AMDAuwDAErwAQCi7AIDuOQAgi7wAAH48AMCJvADAS7oAoM48ACCbPACgyDsAICG8AIAwvQAAqbwAIL08AAABvAAgerwA4NW7AEB/uwCgWDsA4Jc7AEDcvADgRrwAgKY7AODRvABgnbwAYPS7AODRPACA0zwAYDe9AECvOwAgQjwAwLE8ACCqvACgFT0AQJU8AOBFPABA/rsAAKE8ACACPQDgQz0AwIs7AADDPADg/rsAIB49AGBNvQAABz0AoFS7AECCPABg7jwAIFq8AID4ugBg8DwAgIi8AECfOwDAebwAAEW8AIABvABgWLwAIHk8AEDjOwDA9LsAgBg8ACBBvQBgG7wAgJG8AOAJvQAgsTwAwEc8AIBiPABAWDwAAES6AGAuOwAg1zwA4H28ACBNuwAgBrsAILk8ACDLOwBAATwA4Kq8AAA9vABAVjgAgJE7AKAKPADA6DsAgPq8AOBuvACAWjsAIEI8AADRvAAAxjsAIE29AKCzuwBg9rwAwE68AOCCPACgMz0AAKE8ACBBPADguLwAwFC8ACAQPADAaLwAQGA8AOBaPABA7LsAYDI7AOCCPABAb7wAgES8ACAGPQDgU7wAIF68AADhvACAELsA4HE7AADBPACAFz0AYCi7ACDAuwAAlzsAoDk7AAAjOwAAlDwA4Fu8AKACPACgJDwA4DO8AMCtPADgCrwAwLE7AIDrvACAjTwAgMQ8AIAYPACAITwAwDK9AGBPvAAgvLkAAAM8AADnOwDAwjwAQNc5AKCMvADg77sAgCI8AEACPABgorwAgC08AOCLOwDg+bwAoBS9AOB2uwBgBbwAYO27AIB/OgCAb7wAYL06AKD7OQAgu7oA4GE8AGAgPACAWzgA4O+8AIBgOQCARTsAINi8AKAqvADAgbwAYC68AIAIvQAgnDwA4Di8AEA/vADAEr0AoC67AGDlPACgGb0AgOE8AMCmOwCgCboAgM+7AGAHvABABrwAwP67AGC3PABAxjwAoNO8AMAsPACAgbsAIL27AIAEuwBgubwAoIg8AGDKvAAgNbwAwDI9AOADPABgoDwAwPE7AMA+vAAAy7sA4PI7AEDvvADgmzwAIAc8AAAFPADAhzsAYPE8AGAKvQDghTsAgPQ7AEDPuwCgvLwAABG7AAAHvABAsrsAIKO8AGBFvAAglLsAwCs9AOCZuwBATjwAQNu5AGA/uwDgEjkAQPc7AIBaPAAgxDwA4OY7AGAvuwCAAr0AgJU7AGC9uwBAkjwAwNG6AGBzPACgcTwAoCk8AIByvABgl7sAwEM8ACAOPADAGj0AgEI8AMCRPACg6bwA4A48AAA7vAAgJb0AwKo8AGBevAAA8DoAgG+7AACgPADAczwAwIm8AAANPABAGT0AwKS8AKAquwAAPz0AYB+8AAAVvQAgvTwAQBq9AEDyPAAAy7wA4NK6ACChPACAR7wAwFK8AMAWvQBgEjwAQMS6AEDUOwDgjrwAoFG9AMCGPAAg6LsAYAw9AECoPAAAGD0AoBi9AAAsOwAA9rwAAL+8ACCKPADA5LsAAG08AOAJPADABzkAQFw8ACDYPACgAjwAoAO8AIBlOgAgjDwAgCy9AAAZvQCgQbwAYDa8 + index: 0 + model: text-embedding-3-small + usage: + prompt_tokens: 5 + total_tokens: 5 diff --git a/tests/cassettes/embeddings/recorded/openai/invalid-dimensions.yaml b/tests/cassettes/embeddings/recorded/openai/invalid-dimensions.yaml new file mode 100644 index 0000000..25be0b4 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/invalid-dimensions.yaml @@ -0,0 +1,30 @@ +timestamp: '2026-11-03T17:02:25.670767+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: A small public recording. + model: text-embedding-3-small + dimensions: 0 + encoding_format: float +response: + status_code: 400 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_a177d8855a224db0bee2469001674c08 + body_bytes: !!binary | + ewogICJlcnJvciI6IHsKICAgICJtZXNzYWdlIjogIkludmFsaWQgdmFsdWUgZm9yICdkaW1lbnNp + b25zJyA9IDAuIE11c3QgYmUgZ3JlYXRlciB0aGFuIDAuIiwKICAgICJ0eXBlIjogImludmFsaWRf + cmVxdWVzdF9lcnJvciIsCiAgICAicGFyYW0iOiBudWxsLAogICAgImNvZGUiOiBudWxsCiAgfQp9 + body: + error: + message: Invalid value for 'dimensions' = 0. Must be greater than 0. + type: invalid_request_error + param: null + code: null diff --git a/tests/cassettes/embeddings/recorded/openai/manifest.yaml b/tests/cassettes/embeddings/recorded/openai/manifest.yaml new file mode 100644 index 0000000..b0c774b --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/manifest.yaml @@ -0,0 +1,126 @@ +classification: recorded +capture_date: '2026-11-03T17:02:19.497125+00:00' +repository_revision: acf4abe8775e539c3c0aa02c11456054b3ea379b +python: 3.12.9 +versions: + openai: 2.53.0 + httpx: 0.28.1 + pydantic: 2.13.4 +provider: openai +upstream_base_url: https://api.openai.com/v1 +protocol: embeddings +auth: Bearer API key (masked) +sdk_max_retries: 0 +driver: Native AsyncOpenAI.embeddings.create, independent of Molto; request/response + event hooks capture wire JSON and original decoded HTTP response bytes. +scenarios: +- scenario: single-omitted-encoding + file: single-omitted-encoding.yaml + sdk_arguments: + model: text-embedding-3-small + input: Molto preserves vector identity. + dimensions: 8 + result: success + returned_model: text-embedding-3-small + indices: + - 0 + parsed_representation: list + sha256: 4fa16a82166e79ab02b881dca416c97bce30f25744ff48beb7d1946d78167810 +- scenario: batch-float + file: batch-float.yaml + sdk_arguments: + model: text-embedding-3-small + input: + - A query about documents. + - Documents answer the query. + dimensions: 8 + encoding_format: float + user: molto-public-embedding-fixture + result: success + returned_model: text-embedding-3-small + indices: + - 0 + - 1 + parsed_representation: list + sha256: 7821d70c6662c5afec82e7e83c0460d509fcaf62509bdb0f3a08b100c0c9bcfd +- scenario: batch-base64 + file: batch-base64.yaml + sdk_arguments: + model: text-embedding-3-small + input: + - A query about documents. + - Documents answer the query. + dimensions: 8 + encoding_format: base64 + result: success + returned_model: text-embedding-3-small + indices: + - 0 + - 1 + parsed_representation: str + sha256: 1fa3633f540903e87125e2ed48f174e9ff576ef3e137e5b61ad30f89acadee0e +- scenario: token-array-float + file: token-array-float.yaml + sdk_arguments: + model: text-embedding-3-small + input: + - 9906 + - 1917 + dimensions: 8 + encoding_format: float + result: success + returned_model: text-embedding-3-small + indices: + - 0 + parsed_representation: list + sha256: ea18ce7e3886858db52d460b925c704db3de3158507fdb9d6e2eea1a8ca0c7e8 +- scenario: token-batch-base64 + file: token-batch-base64.yaml + sdk_arguments: + model: text-embedding-3-small + input: + - - 9906 + - 1917 + - - 791 + - 407 + dimensions: 8 + encoding_format: base64 + result: success + returned_model: text-embedding-3-small + indices: + - 0 + - 1 + parsed_representation: str + sha256: a9a121aa39bdbdb35d132a2e6c2ee123ff27f21bac4ade2e0cecf8b92dd26153 +- scenario: default-dimensions + file: default-dimensions.yaml + sdk_arguments: + model: text-embedding-3-small + input: A small public recording. + result: success + returned_model: text-embedding-3-small + indices: + - 0 + parsed_representation: list + sha256: 0fd2b657d48387c3a11db613df4d4d618ab17631792d122ce03443a9b0ec829d +- scenario: invalid-dimensions + file: invalid-dimensions.yaml + sdk_arguments: + model: text-embedding-3-small + input: A small public recording. + dimensions: 0 + encoding_format: float + result: upstream_error + error_type: BadRequestError + status_code: 400 + sha256: 96ace4e4ac2f829a45d719e2fb32d8685de0f584bb6f48e0494af62402df3dc7 +- scenario: missing-model + file: missing-model.yaml + sdk_arguments: + model: molto-missing-embedding-model + input: A small public recording. + encoding_format: float + result: upstream_error + error_type: NotFoundError + status_code: 404 + sha256: 797759df9ee4a80c8abdc9351906ab26d317cdc6d2be9a4287e34569b3d4dc5b diff --git a/tests/cassettes/embeddings/recorded/openai/missing-model.yaml b/tests/cassettes/embeddings/recorded/openai/missing-model.yaml new file mode 100644 index 0000000..072fab5 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/missing-model.yaml @@ -0,0 +1,31 @@ +timestamp: '2026-11-03T17:02:25.933590+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: A small public recording. + model: molto-missing-embedding-model + encoding_format: float +response: + status_code: 404 + headers: + content-type: application/json; charset=utf-8 + x-request-id: req_d965126e9e8942e49bdfeb68360de335 + body_bytes: !!binary | + ewogICAgImVycm9yIjogewogICAgICAgICJtZXNzYWdlIjogIlRoZSBtb2RlbCBgbW9sdG8tbWlz + c2luZy1lbWJlZGRpbmctbW9kZWxgIGRvZXMgbm90IGV4aXN0IG9yIHlvdSBkbyBub3QgaGF2ZSBh + Y2Nlc3MgdG8gaXQuIiwKICAgICAgICAidHlwZSI6ICJpbnZhbGlkX3JlcXVlc3RfZXJyb3IiLAog + ICAgICAgICJwYXJhbSI6IG51bGwsCiAgICAgICAgImNvZGUiOiAibW9kZWxfbm90X2ZvdW5kIgog + ICAgfQp9Cg== + body: + error: + message: The model `molto-missing-embedding-model` does not exist or you do + not have access to it. + type: invalid_request_error + param: null + code: model_not_found diff --git a/tests/cassettes/embeddings/recorded/openai/single-omitted-encoding.yaml b/tests/cassettes/embeddings/recorded/openai/single-omitted-encoding.yaml new file mode 100644 index 0000000..bf5ef6e --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/single-omitted-encoding.yaml @@ -0,0 +1,35 @@ +timestamp: '2026-11-03T17:02:19.506506+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: Molto preserves vector identity. + model: text-embedding-3-small + dimensions: 8 + encoding_format: base64 +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_3a99327e26374b75a143f952037a8e32 + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjoiQUtBeVB3RGdVVDRBSUJVOUFHQ0RQZ0RnY2I0QVlITStBT0RrUFFBQUJ6OD0iLCJpbmRleCI6 + MH1dLCJtb2RlbCI6InRleHQtZW1iZWRkaW5nLTMtc21hbGwiLCJ1c2FnZSI6eyJwcm9tcHRfdG9r + ZW5zIjo3LCJ0b3RhbF90b2tlbnMiOjd9fQ== + body: + object: list + data: + - object: embedding + embedding: AKAyPwDgUT4AIBU9AGCDPgDgcb4AYHM+AODkPQAABz8= + index: 0 + model: text-embedding-3-small + usage: + prompt_tokens: 7 + total_tokens: 7 diff --git a/tests/cassettes/embeddings/recorded/openai/token-array-float.yaml b/tests/cassettes/embeddings/recorded/openai/token-array-float.yaml new file mode 100644 index 0000000..3d073eb --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/token-array-float.yaml @@ -0,0 +1,46 @@ +timestamp: '2026-11-03T17:02:24.138966+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: + - 9906 + - 1917 + model: text-embedding-3-small + dimensions: 8 + encoding_format: float +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_57dc65613c51433fb83bcf70fb7a3f90 + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjpbLTAuMDIwMjQ4NDEzMDg1OTM3NSwtMC40NjYzMDg1OTM3NSwwLjE5OTU4NDk2MDkzNzUsMC4y + OTgzMzk4NDM3NSwtMC40MzEzOTY0ODQzNzUsLTAuMjUxMjIwNzAzMTI1LC0wLjI3NTE0NjQ4NDM3 + NSwwLjU3MzI0MjE4NzVdLCJpbmRleCI6MH1dLCJtb2RlbCI6InRleHQtZW1iZWRkaW5nLTMtc21h + bGwiLCJ1c2FnZSI6eyJwcm9tcHRfdG9rZW5zIjoyLCJ0b3RhbF90b2tlbnMiOjJ9fQ== + body: + object: list + data: + - object: embedding + embedding: + - -0.0202484130859375 + - -0.46630859375 + - 0.1995849609375 + - 0.29833984375 + - -0.431396484375 + - -0.251220703125 + - -0.275146484375 + - 0.5732421875 + index: 0 + model: text-embedding-3-small + usage: + prompt_tokens: 2 + total_tokens: 2 diff --git a/tests/cassettes/embeddings/recorded/openai/token-batch-base64.yaml b/tests/cassettes/embeddings/recorded/openai/token-batch-base64.yaml new file mode 100644 index 0000000..e49127d --- /dev/null +++ b/tests/cassettes/embeddings/recorded/openai/token-batch-base64.yaml @@ -0,0 +1,44 @@ +timestamp: '2026-11-03T17:02:25.017855+00:00' +request: + method: POST + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + user-agent: AsyncOpenAI/Python 2.53.0 + body: + input: + - - 9906 + - 1917 + - - 791 + - 407 + model: text-embedding-3-small + dimensions: 8 + encoding_format: base64 +response: + status_code: 200 + headers: + content-type: application/json + openai-version: '2020-10-01' + x-request-id: req_513cfa49904440a9bbcaa8012a68302e + body_bytes: !!binary | + eyJvYmplY3QiOiJsaXN0IiwiZGF0YSI6W3sib2JqZWN0IjoiZW1iZWRkaW5nIiwiZW1iZWRkaW5n + IjoiQU9DbHZBREE3cjRBWUV3K0FNQ1lQZ0RnM0w0QW9JQytBT0NNdmdEQUVqOD0iLCJpbmRleCI6 + MH0seyJvYmplY3QiOiJlbWJlZGRpbmciLCJlbWJlZGRpbmciOiJBR0FDUHdCZ1JUNEF3QXEvQU1C + RVBnQ0FYcjRBNElBK0FJQTNQZ0RBOVQ0PSIsImluZGV4IjoxfV0sIm1vZGVsIjoidGV4dC1lbWJl + ZGRpbmctMy1zbWFsbCIsInVzYWdlIjp7InByb21wdF90b2tlbnMiOjQsInRvdGFsX3Rva2VucyI6 + NH19 + body: + object: list + data: + - object: embedding + embedding: AOClvADA7r4AYEw+AMCYPgDg3L4AoIC+AOCMvgDAEj8= + index: 0 + - object: embedding + embedding: AGACPwBgRT4AwAq/AMBEPgCAXr4A4IA+AIA3PgDA9T4= + index: 1 + model: text-embedding-3-small + usage: + prompt_tokens: 4 + total_tokens: 4 diff --git a/tests/cassettes/embeddings/recorded/voyageai/batch-base64.yaml b/tests/cassettes/embeddings/recorded/voyageai/batch-base64.yaml new file mode 100644 index 0000000..0063ed0 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/batch-base64.yaml @@ -0,0 +1,27 @@ +timestamp: '2026-11-03T17:02:27.313602+00:00' +request: + method: POST + url: https://api.voyageai.com/v1/embeddings + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: voyage-3.5-lite + input: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + input_type: document + truncation: false + output_dimension: 256 + output_dtype: float + encoding_format: base64 +response: + status_code: 401 + headers: + content-type: application/json + body: + detail: Provided API key is invalid. + body_bytes: !!binary | + eyJkZXRhaWwiOiJQcm92aWRlZCBBUEkga2V5IGlzIGludmFsaWQuIn0= diff --git a/tests/cassettes/embeddings/recorded/voyageai/batch-document.yaml b/tests/cassettes/embeddings/recorded/voyageai/batch-document.yaml new file mode 100644 index 0000000..1f5874c --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/batch-document.yaml @@ -0,0 +1,26 @@ +timestamp: '2026-11-03T17:02:26.444757+00:00' +request: + method: POST + url: https://api.voyageai.com/v1/embeddings + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: voyage-3.5-lite + input: + - Penguins live in the Southern Hemisphere. + - Polar bears live in the Arctic. + input_type: document + truncation: false + output_dimension: 256 + output_dtype: float +response: + status_code: 401 + headers: + content-type: application/json + body: + detail: Provided API key is invalid. + body_bytes: !!binary | + eyJkZXRhaWwiOiJQcm92aWRlZCBBUEkga2V5IGlzIGludmFsaWQuIn0= diff --git a/tests/cassettes/embeddings/recorded/voyageai/invalid-model.yaml b/tests/cassettes/embeddings/recorded/voyageai/invalid-model.yaml new file mode 100644 index 0000000..4d9d907 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/invalid-model.yaml @@ -0,0 +1,21 @@ +timestamp: '2026-11-03T17:02:28.201359+00:00' +request: + method: POST + url: https://api.voyageai.com/v1/embeddings + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: molto-invalid-embedding-model + input: Hello + output_dtype: float +response: + status_code: 401 + headers: + content-type: application/json + body: + detail: Provided API key is invalid. + body_bytes: !!binary | + eyJkZXRhaWwiOiJQcm92aWRlZCBBUEkga2V5IGlzIGludmFsaWQuIn0= diff --git a/tests/cassettes/embeddings/recorded/voyageai/manifest.yaml b/tests/cassettes/embeddings/recorded/voyageai/manifest.yaml new file mode 100644 index 0000000..1b2c827 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/manifest.yaml @@ -0,0 +1,25 @@ +recorded_at: '2026-11-03T17:02:28.202716+00:00' +provider: voyageai +deployment: direct hosted API +driver: Standalone httpx POST outside pytest, independent of Molto. No retries. +httpx_version: 0.28.1 +base_url: https://api.voyageai.com/v1/embeddings +provenance: faithful live HTTP recording; only authentication header masked and headers + whitelisted; body_bytes preserves original decoded response bytes +schema_reference: https://docs.voyageai.com/reference/embeddings-api +fixtures: +- file: single-default.yaml + sha256: 57e59d65f8428d2a1b6c2d8ef44a609d97e75f1eb6baf71f244152cc78013056 + status_code: 401 +- file: single-query.yaml + sha256: f49b7c7c6e4202ab2f6476f143949ed29898c1e249ec52017d1580b8e9ad762a + status_code: 401 +- file: batch-document.yaml + sha256: e0e2bd133f8f47d9c525f2cd522c7668f1fbefdb9a5c11df46825ebaebec2455 + status_code: 401 +- file: batch-base64.yaml + sha256: 26bd9e0e90dda4a85ad6190a3b888ae74d6b11e4cef712844bdf09b0d33dea90 + status_code: 401 +- file: invalid-model.yaml + sha256: c8f2c1f1b1cab0276d5fd074b971b1731cd31350abd73e43947a49d4fb014ace + status_code: 401 diff --git a/tests/cassettes/embeddings/recorded/voyageai/single-default.yaml b/tests/cassettes/embeddings/recorded/voyageai/single-default.yaml new file mode 100644 index 0000000..2135af3 --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/single-default.yaml @@ -0,0 +1,23 @@ +timestamp: '2026-11-03T17:02:24.799129+00:00' +request: + method: POST + url: https://api.voyageai.com/v1/embeddings + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: voyage-3.5-lite + input: Where do penguins live? + truncation: false + output_dimension: 256 + output_dtype: float +response: + status_code: 401 + headers: + content-type: application/json + body: + detail: Provided API key is invalid. + body_bytes: !!binary | + eyJkZXRhaWwiOiJQcm92aWRlZCBBUEkga2V5IGlzIGludmFsaWQuIn0= diff --git a/tests/cassettes/embeddings/recorded/voyageai/single-query.yaml b/tests/cassettes/embeddings/recorded/voyageai/single-query.yaml new file mode 100644 index 0000000..38ca79a --- /dev/null +++ b/tests/cassettes/embeddings/recorded/voyageai/single-query.yaml @@ -0,0 +1,24 @@ +timestamp: '2026-11-03T17:02:25.619176+00:00' +request: + method: POST + url: https://api.voyageai.com/v1/embeddings + path: /v1/embeddings + query_params: {} + headers: + authorization: Bearer *** + content-type: application/json + body: + model: voyage-3.5-lite + input: Where do penguins live? + input_type: query + truncation: false + output_dimension: 256 + output_dtype: float +response: + status_code: 401 + headers: + content-type: application/json + body: + detail: Provided API key is invalid. + body_bytes: !!binary | + eyJkZXRhaWwiOiJQcm92aWRlZCBBUEkga2V5IGlzIGludmFsaWQuIn0= diff --git a/tests/cassettes/embeddings/synthetic/infinity/batch-float.json b/tests/cassettes/embeddings/synthetic/infinity/batch-float.json new file mode 100644 index 0000000..f0dbad2 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/batch-float.json @@ -0,0 +1,48 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5, + 0.75 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/dimensions.json b/tests/cassettes/embeddings/synthetic/infinity/dimensions.json new file mode 100644 index 0000000..b82c3a1 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/dimensions.json @@ -0,0 +1,47 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float", + "dimensions": 2 + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/manifest.json b/tests/cassettes/embeddings/synthetic/infinity/manifest.json new file mode 100644 index 0000000..ab54ad8 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/manifest.json @@ -0,0 +1,88 @@ +{ + "kind": "deliberately-synthetic-schema-cases", + "recorded_at": null, + "source": { + "version": "3.0.0", + "commit": "09acbe5cc421b2981d4385f0dbacc868bccd3ae7", + "schema": "https://github.com/michaelfeil/infinity/blob/09acbe5cc421b2981d4385f0dbacc868bccd3ae7/libs/infinity-v3/docs/openapi.json" + }, + "limitations": [ + "Not captured from a running server.", + "schema-test-model is a placeholder; vectors and usage are hand-authored test values, not model output.", + "The pinned server source is a protocol reference, not a live-tested server release." + ], + "cases": [ + { + "file": "single-default.json", + "body": { + "model": "schema-test-model", + "input": "one" + }, + "extra_body": null + }, + { + "file": "batch-float.json", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float" + }, + "extra_body": null + }, + { + "file": "tokens-base64.json", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "base64" + }, + "extra_body": null + }, + { + "file": "token-batch-float.json", + "body": { + "model": "schema-test-model", + "input": [ + [ + 11, + 12 + ], + [ + 21 + ] + ], + "encoding_format": "float" + }, + "extra_body": null + }, + { + "file": "dimensions.json", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float", + "dimensions": 2 + }, + "extra_body": null + }, + { + "file": "user.json", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "float", + "user": "embedding-test" + }, + "extra_body": null + } + ] +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/single-default.json b/tests/cassettes/embeddings/synthetic/infinity/single-default.json new file mode 100644 index 0000000..bcc0262 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/single-default.json @@ -0,0 +1,32 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "base64" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAPgAAgD4AAMA+" + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json b/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json new file mode 100644 index 0000000..ce7ff52 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json @@ -0,0 +1,53 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + [ + 11, + 12 + ], + [ + 21 + ] + ], + "encoding_format": "float" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5, + 0.75 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json b/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json new file mode 100644 index 0000000..9347a54 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "base64" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAPgAAgD4AAMA+" + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/infinity/user.json b/tests/cassettes/embeddings/synthetic/infinity/user.json new file mode 100644 index 0000000..d213c36 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/user.json @@ -0,0 +1,37 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "float", + "user": "embedding-test" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/batch-float.json b/tests/cassettes/embeddings/synthetic/vllm/batch-float.json new file mode 100644 index 0000000..a09ed76 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/batch-float.json @@ -0,0 +1,51 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5, + 0.75 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/dimensions.json b/tests/cassettes/embeddings/synthetic/vllm/dimensions.json new file mode 100644 index 0000000..020c39f --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/dimensions.json @@ -0,0 +1,50 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float", + "dimensions": 2 + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/manifest.json b/tests/cassettes/embeddings/synthetic/vllm/manifest.json new file mode 100644 index 0000000..c60c3bb --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/manifest.json @@ -0,0 +1,104 @@ +{ + "kind": "deliberately-synthetic-schema-cases", + "recorded_at": null, + "source": { + "version": "0.33.0", + "commit": "4ba787ec9959e566d3b587a796dff07673324da5", + "schema": "https://github.com/vllm-project/vllm/blob/4ba787ec9959e566d3b587a796dff07673324da5/vllm/entrypoints/pooling/embed/protocol.py" + }, + "limitations": [ + "Not captured from a running server.", + "schema-test-model is a placeholder; vectors and usage are hand-authored test values, not model output.", + "The pinned server source is a protocol reference, not a live-tested server release." + ], + "cases": [ + { + "file": "single-default.json", + "body": { + "model": "schema-test-model", + "input": "one" + }, + "extra_body": null + }, + { + "file": "batch-float.json", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float" + }, + "extra_body": null + }, + { + "file": "tokens-base64.json", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "base64" + }, + "extra_body": null + }, + { + "file": "token-batch-float.json", + "body": { + "model": "schema-test-model", + "input": [ + [ + 11, + 12 + ], + [ + 21 + ] + ], + "encoding_format": "float" + }, + "extra_body": null + }, + { + "file": "dimensions.json", + "body": { + "model": "schema-test-model", + "input": [ + "one", + "two" + ], + "encoding_format": "float", + "dimensions": 2 + }, + "extra_body": null + }, + { + "file": "user.json", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "float", + "user": "embedding-test" + }, + "extra_body": null + }, + { + "file": "token-options.json", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "float" + }, + "extra_body": { + "add_special_tokens": false, + "truncate_prompt_tokens": 2, + "truncation_side": "left" + } + } + ] +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/single-default.json b/tests/cassettes/embeddings/synthetic/vllm/single-default.json new file mode 100644 index 0000000..910cbcd --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/single-default.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "base64" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAPgAAgD4AAMA+" + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json b/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json new file mode 100644 index 0000000..a1e7ab6 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json @@ -0,0 +1,56 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + [ + 11, + 12 + ], + [ + 21 + ] + ], + "encoding_format": "float" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + }, + { + "object": "embedding", + "index": 1, + "embedding": [ + 0.25, + 0.5, + 0.75 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 4, + "total_tokens": 4, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/token-options.json b/tests/cassettes/embeddings/synthetic/vllm/token-options.json new file mode 100644 index 0000000..5a1c859 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/token-options.json @@ -0,0 +1,45 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "float", + "add_special_tokens": false, + "truncate_prompt_tokens": 2, + "truncation_side": "left" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json b/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json new file mode 100644 index 0000000..4a596a9 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json @@ -0,0 +1,38 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [ + 11, + 12 + ], + "encoding_format": "base64" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAPgAAgD4AAMA+" + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/user.json b/tests/cassettes/embeddings/synthetic/vllm/user.json new file mode 100644 index 0000000..6932bb2 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/user.json @@ -0,0 +1,40 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": "one", + "encoding_format": "float", + "user": "embedding-test" + } + }, + "response": { + "status_code": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.125, + 0.25, + 0.375 + ] + } + ], + "model": "schema-test-model", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2, + "completion_tokens": 0 + }, + "id": "embd-schema-test", + "created": 1793664000 + } + } +} From c881e4ec124774ab0c5cf56f76018509342d5c58 Mon Sep 17 00:00:00 2001 From: Tan Jia Huei Date: Tue, 3 Nov 2026 17:09:23 +0000 Subject: [PATCH 2/3] test: Add native embedding translation fixtures --- .../synthetic/infinity/batch-float.json | 17 +- .../synthetic/infinity/dimensions.json | 15 +- .../synthetic/infinity/manifest.json | 34 +- .../synthetic/infinity/token-batch-float.json | 22 +- .../synthetic/infinity/tokens-base64.json | 5 +- .../embeddings/synthetic/infinity/user.json | 6 +- .../synthetic/infinity/validation-error.json | 22 + .../synthetic/vllm/batch-float.json | 17 +- .../embeddings/synthetic/vllm/dimensions.json | 15 +- .../embeddings/synthetic/vllm/manifest.json | 39 +- .../synthetic/vllm/token-batch-float.json | 22 +- .../synthetic/vllm/token-options.json | 11 +- .../synthetic/vllm/tokens-base64.json | 5 +- .../embeddings/synthetic/vllm/user.json | 6 +- .../synthetic/vllm/validation-error.json | 25 + .../synthetic/voyageai/default-base64.yaml | 31 + .../synthetic/voyageai/default-float.yaml | 542 +++++++++++++++++ .../synthetic/voyageai/document-base64.yaml | 32 ++ .../synthetic/voyageai/document-float.yaml | 543 ++++++++++++++++++ .../synthetic/voyageai/manifest.yaml | 22 + .../synthetic/voyageai/query-base64.yaml | 32 ++ .../synthetic/voyageai/query-float.yaml | 543 ++++++++++++++++++ 22 files changed, 1843 insertions(+), 163 deletions(-) create mode 100644 tests/cassettes/embeddings/synthetic/infinity/validation-error.json create mode 100644 tests/cassettes/embeddings/synthetic/vllm/validation-error.json create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/default-base64.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/default-float.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/document-base64.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/document-float.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/manifest.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/query-base64.yaml create mode 100644 tests/cassettes/embeddings/synthetic/voyageai/query-float.yaml diff --git a/tests/cassettes/embeddings/synthetic/infinity/batch-float.json b/tests/cassettes/embeddings/synthetic/infinity/batch-float.json index f0dbad2..40562ad 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/batch-float.json +++ b/tests/cassettes/embeddings/synthetic/infinity/batch-float.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float" } }, @@ -22,20 +19,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5, - 0.75 - ] + "embedding": [0.25, 0.5, 0.75] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/infinity/dimensions.json b/tests/cassettes/embeddings/synthetic/infinity/dimensions.json index b82c3a1..c174aff 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/dimensions.json +++ b/tests/cassettes/embeddings/synthetic/infinity/dimensions.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float", "dimensions": 2 } @@ -23,18 +20,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25 - ] + "embedding": [0.125, 0.25] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5 - ] + "embedding": [0.25, 0.5] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/infinity/manifest.json b/tests/cassettes/embeddings/synthetic/infinity/manifest.json index ab54ad8..cbb3d2d 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/manifest.json +++ b/tests/cassettes/embeddings/synthetic/infinity/manifest.json @@ -24,10 +24,7 @@ "file": "batch-float.json", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float" }, "extra_body": null @@ -36,10 +33,7 @@ "file": "tokens-base64.json", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "base64" }, "extra_body": null @@ -48,15 +42,7 @@ "file": "token-batch-float.json", "body": { "model": "schema-test-model", - "input": [ - [ - 11, - 12 - ], - [ - 21 - ] - ], + "input": [[11, 12], [21]], "encoding_format": "float" }, "extra_body": null @@ -65,10 +51,7 @@ "file": "dimensions.json", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float", "dimensions": 2 }, @@ -83,6 +66,15 @@ "user": "embedding-test" }, "extra_body": null + }, + { + "file": "validation-error.json", + "body": { + "model": "schema-test-model", + "input": [], + "encoding_format": "float" + }, + "extra_body": null } ] } diff --git a/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json b/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json index ce7ff52..0f86890 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json +++ b/tests/cassettes/embeddings/synthetic/infinity/token-batch-float.json @@ -4,15 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - [ - 11, - 12 - ], - [ - 21 - ] - ], + "input": [[11, 12], [21]], "encoding_format": "float" } }, @@ -27,20 +19,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5, - 0.75 - ] + "embedding": [0.25, 0.5, 0.75] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json b/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json index 9347a54..7143ae3 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json +++ b/tests/cassettes/embeddings/synthetic/infinity/tokens-base64.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "base64" } }, diff --git a/tests/cassettes/embeddings/synthetic/infinity/user.json b/tests/cassettes/embeddings/synthetic/infinity/user.json index d213c36..0fc02a8 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/user.json +++ b/tests/cassettes/embeddings/synthetic/infinity/user.json @@ -20,11 +20,7 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/infinity/validation-error.json b/tests/cassettes/embeddings/synthetic/infinity/validation-error.json new file mode 100644 index 0000000..93967b2 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/infinity/validation-error.json @@ -0,0 +1,22 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [], + "encoding_format": "float" + } + }, + "response": { + "status_code": 400, + "headers": { + "content-type": "application/json" + }, + "body": { + "message": "Batch is empty", + "code": 400, + "error_type": "empty" + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/vllm/batch-float.json b/tests/cassettes/embeddings/synthetic/vllm/batch-float.json index a09ed76..52d12d6 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/batch-float.json +++ b/tests/cassettes/embeddings/synthetic/vllm/batch-float.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float" } }, @@ -22,20 +19,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5, - 0.75 - ] + "embedding": [0.25, 0.5, 0.75] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/vllm/dimensions.json b/tests/cassettes/embeddings/synthetic/vllm/dimensions.json index 020c39f..b4b6aa9 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/dimensions.json +++ b/tests/cassettes/embeddings/synthetic/vllm/dimensions.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float", "dimensions": 2 } @@ -23,18 +20,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25 - ] + "embedding": [0.125, 0.25] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5 - ] + "embedding": [0.25, 0.5] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/vllm/manifest.json b/tests/cassettes/embeddings/synthetic/vllm/manifest.json index c60c3bb..30315cd 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/manifest.json +++ b/tests/cassettes/embeddings/synthetic/vllm/manifest.json @@ -24,10 +24,7 @@ "file": "batch-float.json", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float" }, "extra_body": null @@ -36,10 +33,7 @@ "file": "tokens-base64.json", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "base64" }, "extra_body": null @@ -48,15 +42,7 @@ "file": "token-batch-float.json", "body": { "model": "schema-test-model", - "input": [ - [ - 11, - 12 - ], - [ - 21 - ] - ], + "input": [[11, 12], [21]], "encoding_format": "float" }, "extra_body": null @@ -65,10 +51,7 @@ "file": "dimensions.json", "body": { "model": "schema-test-model", - "input": [ - "one", - "two" - ], + "input": ["one", "two"], "encoding_format": "float", "dimensions": 2 }, @@ -88,10 +71,7 @@ "file": "token-options.json", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "float" }, "extra_body": { @@ -99,6 +79,15 @@ "truncate_prompt_tokens": 2, "truncation_side": "left" } + }, + { + "file": "validation-error.json", + "body": { + "model": "schema-test-model", + "input": [], + "encoding_format": "float" + }, + "extra_body": null } ] } diff --git a/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json b/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json index a1e7ab6..d39f47d 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json +++ b/tests/cassettes/embeddings/synthetic/vllm/token-batch-float.json @@ -4,15 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - [ - 11, - 12 - ], - [ - 21 - ] - ], + "input": [[11, 12], [21]], "encoding_format": "float" } }, @@ -27,20 +19,12 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] }, { "object": "embedding", "index": 1, - "embedding": [ - 0.25, - 0.5, - 0.75 - ] + "embedding": [0.25, 0.5, 0.75] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/vllm/token-options.json b/tests/cassettes/embeddings/synthetic/vllm/token-options.json index 5a1c859..bc8a206 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/token-options.json +++ b/tests/cassettes/embeddings/synthetic/vllm/token-options.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "float", "add_special_tokens": false, "truncate_prompt_tokens": 2, @@ -25,11 +22,7 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json b/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json index 4a596a9..751b2d0 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json +++ b/tests/cassettes/embeddings/synthetic/vllm/tokens-base64.json @@ -4,10 +4,7 @@ "path": "/v1/embeddings", "body": { "model": "schema-test-model", - "input": [ - 11, - 12 - ], + "input": [11, 12], "encoding_format": "base64" } }, diff --git a/tests/cassettes/embeddings/synthetic/vllm/user.json b/tests/cassettes/embeddings/synthetic/vllm/user.json index 6932bb2..417ceab 100644 --- a/tests/cassettes/embeddings/synthetic/vllm/user.json +++ b/tests/cassettes/embeddings/synthetic/vllm/user.json @@ -20,11 +20,7 @@ { "object": "embedding", "index": 0, - "embedding": [ - 0.125, - 0.25, - 0.375 - ] + "embedding": [0.125, 0.25, 0.375] } ], "model": "schema-test-model", diff --git a/tests/cassettes/embeddings/synthetic/vllm/validation-error.json b/tests/cassettes/embeddings/synthetic/vllm/validation-error.json new file mode 100644 index 0000000..91b3744 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/vllm/validation-error.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "path": "/v1/embeddings", + "body": { + "model": "schema-test-model", + "input": [], + "encoding_format": "float" + } + }, + "response": { + "status_code": 400, + "headers": { + "content-type": "application/json" + }, + "body": { + "error": { + "message": "Input cannot be empty", + "type": "BadRequestError", + "param": null, + "code": 400 + } + } + } +} diff --git a/tests/cassettes/embeddings/synthetic/voyageai/default-base64.yaml b/tests/cassettes/embeddings/synthetic/voyageai/default-base64.yaml new file mode 100644 index 0000000..0281b40 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/default-base64.yaml @@ -0,0 +1,31 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false + encoding_format: base64 +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPw== + trace: 1 + - object: embedding + index: 0 + embedding: AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvw== + trace: 0 + request_id: synthetic-voyage diff --git a/tests/cassettes/embeddings/synthetic/voyageai/default-float.yaml b/tests/cassettes/embeddings/synthetic/voyageai/default-float.yaml new file mode 100644 index 0000000..45664e9 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/default-float.yaml @@ -0,0 +1,542 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + trace: 1 + - object: embedding + index: 0 + embedding: + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + trace: 0 + request_id: synthetic-voyage diff --git a/tests/cassettes/embeddings/synthetic/voyageai/document-base64.yaml b/tests/cassettes/embeddings/synthetic/voyageai/document-base64.yaml new file mode 100644 index 0000000..85fb1fd --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/document-base64.yaml @@ -0,0 +1,32 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false + input_type: document + encoding_format: base64 +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPw== + trace: 1 + - object: embedding + index: 0 + embedding: AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvw== + trace: 0 + request_id: synthetic-voyage diff --git a/tests/cassettes/embeddings/synthetic/voyageai/document-float.yaml b/tests/cassettes/embeddings/synthetic/voyageai/document-float.yaml new file mode 100644 index 0000000..bcbc4bf --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/document-float.yaml @@ -0,0 +1,543 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false + input_type: document +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + trace: 1 + - object: embedding + index: 0 + embedding: + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + trace: 0 + request_id: synthetic-voyage diff --git a/tests/cassettes/embeddings/synthetic/voyageai/manifest.yaml b/tests/cassettes/embeddings/synthetic/voyageai/manifest.yaml new file mode 100644 index 0000000..c96eef6 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/manifest.yaml @@ -0,0 +1,22 @@ +provenance: Deliberately synthetic Voyage native schema cases. No response was produced + by Molto or a live provider. Success recording unavailable because direct API key + returned HTTP401. +schema_reference: https://docs.voyageai.com/reference/embeddings-api +schema_review_date: '2026-11-03' +model: voyage-3.5-lite +dimensions: 256 +vectors: Repeated [0.25, -0.5] and [0.75, 1.0] 128 times; base64 is little-endian + float32; reversed data indices deliberately test correspondence. +fixtures: +- file: default-float.yaml + sha256: b1dde5565856577fb6f456b3dfbbcb7e5ec705e65fe88a7f898109d782c8570a +- file: default-base64.yaml + sha256: 21dc61bfcec04b4f68cf8a228d33b31f3064ecfa26ddab5b464d760527a95d99 +- file: query-float.yaml + sha256: 352aa85154c6774c6dc3715ca087bb8a14a6b8df89d64e4ba76ffffee350b335 +- file: query-base64.yaml + sha256: 57b14df7798054ad59e48a3cb561b20dbba8ed0dcc18dce8e2ed20b98542c70c +- file: document-float.yaml + sha256: f94e5baef06a09876882b1febba96498814ce7cf7bc7199670e44bc031a8d5cf +- file: document-base64.yaml + sha256: d5c38ad5670e13fc324086e94488b232906f4373e5a4a4670324c3113c84dfbb diff --git a/tests/cassettes/embeddings/synthetic/voyageai/query-base64.yaml b/tests/cassettes/embeddings/synthetic/voyageai/query-base64.yaml new file mode 100644 index 0000000..5bf4750 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/query-base64.yaml @@ -0,0 +1,32 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false + input_type: query + encoding_format: base64 +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPwAAQD8AAIA/AABAPwAAgD8AAEA/AACAPw== + trace: 1 + - object: embedding + index: 0 + embedding: AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvwAAgD4AAAC/AACAPgAAAL8AAIA+AAAAvw== + trace: 0 + request_id: synthetic-voyage diff --git a/tests/cassettes/embeddings/synthetic/voyageai/query-float.yaml b/tests/cassettes/embeddings/synthetic/voyageai/query-float.yaml new file mode 100644 index 0000000..9e146e7 --- /dev/null +++ b/tests/cassettes/embeddings/synthetic/voyageai/query-float.yaml @@ -0,0 +1,543 @@ +provenance: Deliberately synthetic native schema case; vectors, usage, reversed order + and extensions are invented test values, never live provider evidence. +request: + method: POST + path: /v1/embeddings + body: + model: voyage-3.5-lite + input: + - first + - second + output_dimension: 256 + output_dtype: float + truncation: false + input_type: query +response: + status_code: 200 + body: + object: list + model: voyage-3.5-lite + usage: + total_tokens: 7 + data: + - object: embedding + index: 1 + embedding: + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + - 0.75 + - 1.0 + trace: 1 + - object: embedding + index: 0 + embedding: + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + - 0.25 + - -0.5 + trace: 0 + request_id: synthetic-voyage From 63b3e8e7f21295b0439b07588d181c69cb239e1b Mon Sep 17 00:00:00 2001 From: Tan Jia Huei Date: Tue, 3 Nov 2026 17:11:54 +0000 Subject: [PATCH 3/3] feat: Add dense embedding integrations --- .env.example | 2 + README.md | 15 + docs/README.md | 5 +- docs/errors.md | 8 + docs/provider-embeddings.md | 124 ++++++ python/ARCHITECTURE.md | 18 +- python/CONVENTIONS.md | 4 +- python/molto/adapters/embeddings.py | 34 ++ python/molto/configs.py | 10 + python/molto/core.py | 90 +++- python/molto/providers.py | 13 + python/molto/types/embeddings.py | 39 ++ python/molto/types/openai_embeddings.py | 21 + python/molto/upstream/base.py | 37 +- python/molto/upstream/embeddings.py | 296 +++++++++++++ tests/cassettes/README.md | 8 + .../synthetic/infinity/validation-error.json | 2 +- tests/core/test_embedding_engines.py | 177 ++++++++ tests/core/test_embeddings.py | 380 +++++++++++++++++ tests/test_embedding_translation.py | 399 ++++++++++++++++++ 20 files changed, 1668 insertions(+), 14 deletions(-) create mode 100644 docs/provider-embeddings.md create mode 100644 python/molto/adapters/embeddings.py create mode 100644 python/molto/types/embeddings.py create mode 100644 python/molto/types/openai_embeddings.py create mode 100644 python/molto/upstream/embeddings.py create mode 100644 tests/core/test_embedding_engines.py create mode 100644 tests/core/test_embeddings.py create mode 100644 tests/test_embedding_translation.py diff --git a/.env.example b/.env.example index a7c0fa0..18ebbec 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,5 @@ MOLTO_LOG_LEVEL_STDERR=INFO # External API keys ANTHROPIC_API_KEY= OPENAI_API_KEY= +COHERE_API_KEY= +VOYAGE_API_KEY= diff --git a/README.md b/README.md index 77dccb0..bd0b031 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,21 @@ asyncio.run(main()) These settings are independent. Use the same protocol for the method and `endpoint` to get native SDK passthrough. The returned `MoltoStream.response_headers` retains successful upstream HTTP headers for callers such as proxy servers. +## Embeddings + +`Molto.embeddings()` accepts an OpenAI embeddings body and returns the parsed OpenAI SDK response. With `OPENAI_API_KEY` configured: + +```python +async with Molto() as molto: + result = await molto.embeddings( + {"model": "text-embedding-3-small", "input": ["A document", "A question"]} + ) + for item in result.data: + print(item.index, len(item.embedding)) +``` + +The [embedding guide and support matrix](docs/provider-embeddings.md) covers OpenAI, Cohere, VoyageAI, Infinity, and vLLM, including native task options and evidence limitations. OpenAI and Cohere have successful live recordings; VoyageAI, Infinity, and vLLM require further live verification. Embeddings use one ordinary asynchronous request, without streaming or automatic batching. + ## Error handling Molto exposes recognized errors in the downstream SDK namespace, with a normalized Molto cause for portable classification. HTTP errors preserve their upstream status and body. diff --git a/docs/README.md b/docs/README.md index d43c5b4..6dcdf35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,9 @@ Molto has two design goals: - Support any implemented downstream protocol through any implemented upstream protocol with one provider-neutral translation model. - Bypass translation when both sides use the same protocol, preserving native requests, events, and responses with the [shared error policy](errors.md). -Streaming and non-streaming requests are supported on native and translated routes. Streaming is the translated execution primitive; translated non-streaming responses are collected from the downstream-native event stream produced by the same event projector. +Conversational streaming and non-streaming requests are supported on native and translated routes. Streaming is the conversational translated execution primitive; translated non-streaming responses are collected from the downstream-native event stream produced by the same event projector. + +`Molto.embeddings()` provides a separate OpenAI-shaped dense embedding API, with native OpenAI-compatible transport and Cohere/VoyageAI translation. It does not use conversational messages or streams. See the [five-provider support matrix, examples, and evidence limits](provider-embeddings.md). ## Cross-protocol contract @@ -32,6 +34,7 @@ The public method selects the protocol returned to the application. The `endpoin | OpenAI Responses | `Molto.responses()` | `responses` | OpenAI Responses SDK events | OpenAI SDK `Response` | [OpenAI Responses](protocols/openai-responses.md) | | OpenAI Chat Completions | `Molto.chat_completions()` | `chat/completions` | OpenAI `ChatCompletionChunk` values | OpenAI SDK `ChatCompletion` | [OpenAI Chat Completions](protocols/openai-chat-completions.md) | | Anthropic Messages | `Molto.messages()` | `messages` | Anthropic Messages SDK events | Anthropic SDK `Message` | [Anthropic Messages](protocols/anthropic-messages.md) | +| OpenAI Embeddings | `Molto.embeddings()` | `embeddings` | Not supported | OpenAI SDK `CreateEmbeddingResponse` | [Embeddings](provider-embeddings.md) | Use Responses when an application benefits from typed output items and a rich streaming lifecycle. Use Chat Completions for applications built around the widely supported `messages` and choice-delta interface. Use Messages for applications that need the Anthropic SDK contract and its content-block model. The selected upstream endpoint does not have to match that choice. diff --git a/docs/errors.md b/docs/errors.md index 5154274..0c6c0ce 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -55,6 +55,14 @@ Molto deliberately raises the downstream SDK's `AuthenticationError` instead of The upstream client constructs the intended request using its effective base URL and endpoint, preserving SDK path-prefix and trailing-slash behavior. The adapter wraps it in the synthetic response. +## Embeddings + +Embedding clients reuse OpenAI SDK HTTP transport for all three wire protocols. Errors therefore use OpenAI SDK classes, including native Cohere and VoyageAI responses. The provider status, body, response bytes, and URL remain intact. Codes `model_not_found` and `context_length_exceeded` use their corresponding normalized categories; SDK authentication and not-found classes use `AuthenticationError` and `ResourceNotFoundError`. An unknown provider error is not classified by guessed message text. Other SDK errors, including rate limits and transport failures, pass through unchanged. + +Missing embedding keys fail locally before the SDK can fall back to an unrelated OpenAI environment key, using the same synthetic-401 convention above. Self-hosted routes require an explicit base URL and use the non-secret `EMPTY` key when none is supplied. Unsupported task options, input forms, or output encodings raise `UnsupportedInput` or public request validation errors before HTTP; malformed output or inconsistent vector correspondence raises output-side validation errors. + +Embedding replay covers real OpenAI invalid-dimensions/missing-model errors, Cohere missing-model errors, and VoyageAI authentication failures. Engine validation errors and unavailable native-provider success cases use explicitly synthetic schema fixtures; they are not live verification. See the [embedding evidence matrix](provider-embeddings.md). + ## Implementation and verification See [error translation conventions](../python/CONVENTIONS.md#error-translation) for implementation and [error cassettes](../tests/cassettes/README.md#error-cassettes) for replay requirements. Cassette-backed coverage includes streaming and non-streaming requests across native and translated routes for every error scenario. Non-streaming tests use non-streaming recordings and compare provider SDK exceptions and their normalized cause chains; translated calls still stream upstream internally. Missing-credential tests cover both result modes and assert that no HTTP request is sent. diff --git a/docs/provider-embeddings.md b/docs/provider-embeddings.md new file mode 100644 index 0000000..97166b5 --- /dev/null +++ b/docs/provider-embeddings.md @@ -0,0 +1,124 @@ +# Embeddings + +`await Molto.embeddings(...)` accepts an OpenAI-shaped embedding request and returns an OpenAI SDK `CreateEmbeddingResponse`. It supports dense text embeddings through five explicit provider integrations. It does not stream, split batches, tokenize, truncate, pool vectors, or submit asynchronous jobs. An ordinary asynchronous Python call waits for one upstream HTTP response. + +The implementation and offline coverage extend beyond the available live evidence. OpenAI and Cohere have successful live recordings. VoyageAI has authentication-error recordings and synthetic success coverage. Infinity and vLLM have pinned schema references and synthetic coverage, but **no live-tested server version or model**. These gaps remain part of [issue #93](https://github.com/EmbeddedLLM/molto/issues/93). + +## Public contract + +```python +from molto import Molto + +async with Molto() as molto: + result = await molto.embeddings( + { + "model": "text-embedding-3-small", + "input": ["A document about planets", "A document about stars"], + "dimensions": 256, + } + ) + vectors_by_input = {item.index: item.embedding for item in result.data} +``` + +The request body has the same body fields as the pinned OpenAI SDK: required `model` and `input`, and optional `dimensions`, `encoding_format`, and `user`. `OpenAIEmbeddingsCreateRequest` is available from `molto.types.openai_embeddings`. Inputs may be a string, a list of strings, one list of token IDs, or a batch of token-ID lists. Token IDs must be integers; Molto does not coerce strings, floats, or booleans into IDs. Cohere and VoyageAI support only the text forms in this coordinator. + +`endpoint`, `provider`, and `base_url` have separate responsibilities: + +| Argument | Meaning | +| --- | --- | +| `endpoint` | Selects `embeddings`, `cohere/embed`, or `voyage/embeddings`; defaults to `embeddings`. These are protocol identifiers, not literal URL paths. | +| `provider` | Selects `openai`, `cohere`, `voyageai`, `infinity`, or `vllm`. When omitted, defaults to the canonical provider for the endpoint. | +| `base_url` | Overrides only the destination and may include a deployment path prefix. Infinity and vLLM require it explicitly. | +| `api_key` | Overrides the configured provider key. Self-hosted routes use the non-secret `EMPTY` credential when omitted. | +| `options` | Accepts the matching typed `CohereEmbeddingOptions` or `VoyageEmbeddingOptions` on the corresponding native endpoint. | +| `extra_body` | Applies upstream body extensions after request projection. The effective body is checked for supported embedding semantics. | +| `extra_headers` | Adds request-scoped upstream headers after Molto removes credentials, account selectors, connection headers, and ingress transport headers. | + +Changing a provider never silently changes the endpoint. For example, `provider="cohere"` requires `endpoint="cohere/embed"`. A custom URL does not imply compatibility with a different provider or protocol. OpenAI account environment headers are suppressed on compatible routes, and explicit credentials cannot be replaced through `extra_headers`. + +## Evidence and support matrix + +Evidence was collected on 2026-11-03. A schema reference is not a claim that its server release or any model was executed. Synthetic vectors are small hand-authored values that test transport and parsing; they are not embedding-quality evidence. + +| Provider | Endpoint and route | Authentication | Model and version evidence | Covered boundary | +| --- | --- | --- | --- | --- | +| OpenAI | `embeddings`; `https://api.openai.com/v1/embeddings` | Bearer; `OPENAI_API_KEY` | Live `text-embedding-3-small`; parsed SDK 2.53.0 | Scalar/batched text, single/batched token IDs, omitted/float/base64 encoding, default 1536 and explicit 8 dimensions, `user`, native usage, 400/404 errors. | +| Cohere | `cohere/embed`; `https://api.cohere.com/v2/embed` | Bearer; `COHERE_API_KEY` | Live `embed-v4.0`, API v2; official native response schema | Text query/document, classification/clustering, `truncate` values `NONE`/`START`/`END`, 256 dimensions, float/base64, billed-unit metadata, model error. | +| VoyageAI | `voyage/embeddings`; `https://api.voyageai.com/v1/embeddings` | Bearer; `VOYAGE_API_KEY` | Live attempts returned 401; successful response behavior is synthetic, schema-based | Text query/document intent, dimensions, float/base64, truncation, source-backed token totals and native metadata. No successful live model verification. | +| Infinity | `embeddings`; caller-supplied base ending in `/v1`, then `/embeddings` | Configured bearer or `EMPTY` | Schema pinned to **3.0.0**, commit `09acbe5`; no live server/model test | Synthetic dense text and token-ID forms, dimensions, encoding defaults/overrides, `user`, usage, and native errors. | +| vLLM | `embeddings`; caller-supplied base ending in `/v1`, then `/embeddings` | Configured bearer or `EMPTY` | Schema pinned to **0.33.0**, commit `4ba787e`; configured server connection timed out | Synthetic dense text and token-ID forms, dimensions, encoding defaults/overrides, special-token/truncation controls, native indices, usage, IDs, timestamps, and errors. | + +The live captures establish only the listed model/options, not every value accepted by the schema. Cohere accepted all three truncation options, but these short inputs do not demonstrate actual truncation outcomes, other dimensions, or model limits. + +The self-hosted fixtures use `schema-test-model` deliberately. This is not a downloadable model or a server configuration example. Choose a deployed dense embedding model and verify its dimensionality, tokenizer, input limits, and task preparation against that deployment before relying on it. + +## Encodings and response fidelity + +On OpenAI-compatible routes, the SDK owns response parsing and Molto returns its response object directly. Omitted `encoding_format` follows the parsed SDK contract: the SDK requests base64 on the wire and decodes float32 data into Python floats. Explicit `float` preserves float vectors; explicit `base64` preserves the encoded strings. An `extra_body` encoding override changes the wire request but preserves SDK parsing behavior: when the typed `encoding_format` argument was omitted, the SDK still decodes base64 to floats; an explicit typed encoding disables that automatic decoding. For example, omitted typed encoding plus `extra_body={"encoding_format": "base64"}` still returns floats, while typed `encoding_format="float"` with that same override returns strings, just like the SDK. + +Cohere and VoyageAI use their native wire formats. Molto maps only dense vectors into the OpenAI-shaped result. Omitted or explicit `float` returns float vectors; explicit `base64` returns base64 strings. On these translated routes, the effective encoding after `extra_body` overrides determines the output representation. Integer, binary, sparse, tensor, and multiple-vector results are unsupported, rather than coerced into dense float vectors. Non-float32 encodings and incompatible byte order are rejected on compatible routes. + +Each vector retains its input index. Native indexed responses retain their response order and original indices, even if the upstream returns indices out of order; use `item.index` to associate vectors with inputs. Cohere's ordered vector list receives corresponding zero-based indices. Molto does not automatically split requests or combine inputs. + +Native responses preserve the returned model, usage, and SDK extension fields. Translated results retain native response metadata under `provider_metadata`, including Cohere's ID, echoed texts, and `meta`, and VoyageAI's unmapped fields and vector metadata. When a native response omits the model name, as Cohere does, its result identifies the effective requested model; Molto does not invent a resolved model revision. + +Missing token usage remains unknown. In particular, Cohere's `meta.billed_units` is preserved as billing metadata and is not relabeled as actual token usage; the OpenAI-shaped `usage` is `None`. VoyageAI's reported `total_tokens` is preserved, while `prompt_tokens` remains `None` when unavailable. These `None` values are intentional extensions to the SDK's static annotations. Do not replace them with zero or infer that vectors from different models inhabit the same space. + +## Native task and truncation controls + +```python +from molto import Molto +from molto.types.embeddings import CohereEmbeddingOptions, VoyageEmbeddingOptions + +async with Molto() as molto: + cohere_result = await molto.embeddings( + {"model": "embed-v4.0", "input": ["A document about planets"], "dimensions": 256}, + endpoint="cohere/embed", + options=CohereEmbeddingOptions(input_type="search_document", truncate="NONE"), + ) + voyage_result = await molto.embeddings( + {"model": "voyage-3.5", "input": ["A document about planets"]}, + endpoint="voyage/embeddings", + options=VoyageEmbeddingOptions(input_type="document", truncation=False), + ) +``` + +The Voyage example describes the implemented API; its success has not been verified with the available credentials. + +Cohere options use its native `input_type`: `search_query`, `search_document`, `classification`, or `clustering`. An input type is required. `truncate` accepts `NONE`, `START`, or `END`; omission leaves the upstream default. The input string or list becomes `texts`, `dimensions` becomes `output_dimension`, and the requested encoding selects one `embedding_types` entry. + +Voyage options accept optional `input_type="query"` or `"document"` and optional boolean `truncation`. Molto preserves the input string/list shape, maps `dimensions` to `output_dimension`, and requests float output data. It does not treat Voyage's boolean truncation as equivalent to Cohere's directional truncation. Omitted options leave the provider defaults intact. Both translated routes reject `user` and token-ID input because no supported semantic mapping exists. + +On vLLM, compatible native extensions such as `add_special_tokens`, `truncate_prompt_tokens`, and `truncation_side` may be supplied in `extra_body`; they are forwarded unchanged. This is provider-specific transport, not a portable query/document abstraction. Molto never inserts textual query prefixes, prepends titles, or changes token sequences to imitate an option. Unsupported typed fields fail validation; unknown native extensions remain the upstream service's responsibility. + +## Modality and contextual boundaries + +This coordinator supports one dense vector per text or supported token-ID input. It does not claim complete modality coverage for any provider. + +| Provider | Modes outside this coordinator | +| --- | --- | +| OpenAI | Image, audio, file, conversation, and contextual-group input are not part of the embedding request contract. | +| Cohere | Embed v4 image-only and mixed text/image `inputs`, including multiple content parts combined into one vector, are unsupported here. Binary/integer outputs and multiple simultaneous representations are unsupported. | +| VoyageAI | Multimodal and contextual embedding endpoints, document/chunk grouping, file metadata, and integer/binary output types are unsupported. | +| Infinity | Conversation `messages`, multimodal content, tensor and multiple-vector outputs, and separate sparse/token-level endpoints are unsupported. | +| vLLM | Conversation/multimodal embeddings, token embeddings, late chunking, binary-response modes, and alternate embedding data types are unsupported. | + +The request schema rejects non-text content objects and nested contextual string groups. Known unsupported modes in `extra_body` also fail explicitly. Output validation rejects vector matrices, tensor objects, and sparse structures without flattening or pooling them. An unrecognized provider extension is not a support claim for a new modality. + +## Limits, errors, and lifecycle + +Providers and models own batch-size, token, dimension, and truncation limits. Molto forwards supported requests as one call and preserves upstream errors; it does not enforce a stale local model catalog or silently split an oversized batch. OpenAI currently documents 8192 tokens per input, at most 2048 inputs, and 300000 tokens across a request. Cohere documents at most 96 texts per call. Voyage limits depend on the model and request token total. Self-hosted limits depend on the selected model and server configuration. Consult the linked API references for current values. + +OpenAI-compatible HTTP errors retain the SDK error contract and pass through Molto's existing recognized-error translation. Native Cohere/Voyage HTTP failures surface in the OpenAI error namespace with the real upstream request, response, status, and body retained. Missing credentials fail locally before HTTP. Unsupported translation raises `UnsupportedInput`; malformed output raises an output validation error. + +All routes borrow Molto's shared HTTP client and respect its lifecycle. `Molto.aclose()` closes that client. Configure HTTP timeouts on the injected `httpx.AsyncClient`. There is no embedding stream to close and no background job to poll. + +## Sources and replay + +- [OpenAI embedding API](https://developers.openai.com/api/reference/resources/embeddings/methods/create) and the repository-pinned OpenAI SDK define the public baseline. +- [Cohere Embed API v2](https://docs.cohere.com/reference/embed) defines its native task, encoding, and billing fields. +- [Voyage embeddings API](https://docs.voyageai.com/reference/embeddings-api) defines text embedding options and usage. +- [Infinity 3.0.0 OpenAPI schema](https://github.com/michaelfeil/infinity/blob/09acbe5cc421b2981d4385f0dbacc868bccd3ae7/libs/infinity-v3/docs/openapi.json) pins the self-hosted protocol reference. +- [vLLM 0.33.0 embedding protocol](https://github.com/vllm-project/vllm/blob/4ba787ec9959e566d3b587a796dff07673324da5/vllm/entrypoints/pooling/embed/protocol.py), [shared request options](https://github.com/vllm-project/vllm/blob/4ba787ec9959e566d3b587a796dff07673324da5/vllm/entrypoints/pooling/base/protocol.py), and [token embedding protocol](https://github.com/vllm-project/vllm/blob/4ba787ec9959e566d3b587a796dff07673324da5/vllm/entrypoints/pooling/token_embed/protocol.py) distinguish supported dense requests from excluded modes. + +Fixtures live under `tests/cassettes/embeddings/recorded/` and `tests/cassettes/embeddings/synthetic/`. Manifests state provenance, options, versions, and limitations. Recorded requests were made outside pytest, independently of Molto, and reviewed for secrets. Tests only replay fixtures, assert exact upstream bodies, and compare compatible results against parsed OpenAI SDK responses using the same fixture. They never compare independently generated vectors across providers or replace expected output with Molto's own output. diff --git a/python/ARCHITECTURE.md b/python/ARCHITECTURE.md index 9664a86..b2d8ba7 100644 --- a/python/ARCHITECTURE.md +++ b/python/ARCHITECTURE.md @@ -4,7 +4,9 @@ Molto is an LLM protocol translation library and the facade for its public coordinators. Its implemented `Molto.responses()`, `Molto.chat_completions()`, and `Molto.messages()` methods each accept one native protocol and return that same protocol to the caller. -Three upstream endpoints are implemented: +`Molto.embeddings()` accepts an OpenAI embeddings request and returns an OpenAI SDK `CreateEmbeddingResponse`. Its separate non-streaming path supports dense embeddings through OpenAI-compatible, Cohere v2, and VoyageAI protocols. The [embedding matrix](../docs/provider-embeddings.md) distinguishes implemented routes, faithful live recordings, and synthetic schema coverage. + +Three conversational upstream endpoints are implemented: - `responses`: native Responses passthrough or cross-protocol translation through Responses; - `chat/completions`: native Chat Completions passthrough or cross-protocol translation through Chat Completions; @@ -123,6 +125,20 @@ Adapters and upstream clients do not depend on each other. Protocol-specific coo Upstream clients normalize recognized SDK failures as `APIException`; downstream adapters project them into their SDK namespace. Both native and translated routes use this pipeline for streaming and non-streaming requests. See [Errors](../docs/errors.md) for recognition rules, status mappings, and special cases. +## Embeddings + +Embedding routes have separate `EmbeddingEndpoint` and `EmbeddingProvider` types. `embeddings` selects the OpenAI wire protocol; `cohere/embed` selects Cohere v2 Embed; `voyage/embeddings` selects VoyageAI's embedding protocol. These names select protocols, not literal URL paths. The coordinator rejects mismatched providers and requires explicit destinations for Infinity and vLLM. It never infers protocol support from a URL or switches endpoints. + +`OpenAIEmbeddingsCreateRequest` tracks the pinned SDK's five body parameters. Strict scalar types preserve strings, batches, token IDs, and token batches without coercion. `CohereEmbeddingOptions` and `VoyageEmbeddingOptions` retain native task and truncation vocabularies rather than claiming equivalence between similarly named controls. Extra body fields remain request-scoped upstream transport options, applied after projection under the restrictions in the embedding guide. + +`BaseHTTPClient` owns the shared HTTP transport configuration, SDK construction, and URL joining. Conversational `BaseClient` adds reasoning capabilities and event lifecycle methods. Embedding clients inherit the transport without implementing artificial streams. Every SDK wrapper borrows the facade's HTTP client; only `Molto.aclose()` closes it. + +The native embedding client delegates to `AsyncOpenAI.embeddings.create()` and returns its parsed response directly. In particular, omission of `encoding_format` remains omitted at the method boundary, allowing the SDK to request base64 and decode it to float32 values. Explicit base64 remains a string. Native indices, order, usage, model identity, and provider extensions remain intact. + +Cohere and VoyageAI clients construct their own wire requests and reuse the SDK's generic HTTP transport, retries, and status exceptions. Their small `NormalizedEmbeddingResponse` carries indexed dense vectors, source model identity, optional usage, and provider metadata. The embedding adapter projects that result into the OpenAI SDK model. Cohere's response has no model identity, so its effective requested model is retained; billed units remain metadata, never fabricated token usage. An unavailable usage value is explicitly `None` at the public boundary. Request and result cardinality checks protect input correspondence on translated routes. + +This path does not tokenize, truncate, split batches, pool vectors, or emulate asynchronous provider jobs. Multimodal inputs, contextual groups, sparse outputs, quantized vectors, and multiple vectors per input are outside the declared dense text contract and rejected where their semantics cannot be preserved. It introduces no conversational IR fields or events. + ## Orchestration facade and coordinators `Molto` is expected to expose one coordinator method per downstream protocol: diff --git a/python/CONVENTIONS.md b/python/CONVENTIONS.md index 0a8394f..9372ccc 100644 --- a/python/CONVENTIONS.md +++ b/python/CONVENTIONS.md @@ -36,7 +36,7 @@ Keep upstream clients specific to one supported wire protocol; never switch or o Create a fresh `BaseAdapter[NativeRequestT, NativeEventT, NativeResponseT]` subclass instance for each translated request, specifying its concrete native request, event, and response types. Call its public `normalize_request()` before `project_events()` or `project_response()` so it can retain the validated downstream model as `native_request`. Implement protocol-specific translation in `_normalize_request()`, `_project_events()`, and `_project_response()`: `_project_events()` consumes normalized events and yields downstream-native events, while `_project_response()` consumes that projected native event stream and returns the downstream-native response. Translate known IR failures into downstream-native exceptions in `_handle_error()`. Re-raise errors without a native equivalent. Do not override the public lifecycle methods or bypass their state and error handling. Same-protocol passthrough composes `project_native_events()` or `project_native_response()` so native failures receive the same downstream-native error projection. -Every upstream client must subclass `BaseClient[NativeRequestT, NativeEventT, NativeResponseT]`, specify its concrete native types, and implement its native paths, normalized path, and error translation: +Every conversational upstream client must subclass `BaseClient[NativeRequestT, NativeEventT, NativeResponseT]`, specify its concrete native types, and implement its native paths, normalized path, and error translation: - `native_events()` accepts the validated upstream-native request, yields native SDK events, and delegates failures to `_handle_error()`; - `native_response()` accepts the validated upstream-native request, returns a non-streaming SDK response, and delegates failures to `_handle_error()`; @@ -45,6 +45,8 @@ Every upstream client must subclass `BaseClient[NativeRequestT, NativeEventT, Na The public `events()` wrapper owns error interception; do not override it. Clients do not retain adapter request state. Pass normalized requests explicitly and keep downstream-native request handling in the adapter. +Embedding clients inherit `BaseHTTPClient`, sharing transport and client ownership without artificial event methods. Keep vectors and embedding metadata in the separate embedding types. Native OpenAI embeddings return the SDK result directly; translated native-provider responses pass through the embedding response adapter. Preserve the SDK's omitted-encoding behavior and keep native task options distinct. Unknown token usage is `None`, not zero or billed units. See the [embedding contract](../docs/provider-embeddings.md). + ## Error translation Follow the [error contract and recognition rules](../docs/errors.md): diff --git a/python/molto/adapters/embeddings.py b/python/molto/adapters/embeddings.py new file mode 100644 index 0000000..e89985c --- /dev/null +++ b/python/molto/adapters/embeddings.py @@ -0,0 +1,34 @@ +"""Project dense embedding output without inventing missing provider usage.""" + +from openai.types import CreateEmbeddingResponse, Embedding +from openai.types.create_embedding_response import Usage + +from molto.types.embeddings import NormalizedEmbeddingResponse + + +def project_embeddings_response(response: NormalizedEmbeddingResponse) -> CreateEmbeddingResponse: + """Return SDK models, retaining unavailable counts as None and native metadata.""" + return CreateEmbeddingResponse.model_construct( + object="list", + model=response.model, + data=[ + Embedding.model_construct( + object="embedding", + index=item.index, + embedding=item.embedding, + **( + {"provider_metadata": item.provider_metadata} if item.provider_metadata else {} + ), + ) + for item in response.data + ], + usage=( + Usage.model_construct( + prompt_tokens=response.usage.prompt_tokens, + total_tokens=response.usage.total_tokens, + ) + if response.usage is not None + else None + ), + provider_metadata=response.provider_metadata, + ) diff --git a/python/molto/configs.py b/python/molto/configs.py index a03d1ff..dbfeb8e 100644 --- a/python/molto/configs.py +++ b/python/molto/configs.py @@ -24,11 +24,21 @@ class EnvConfig(BaseSettings): molto_log_level_stderr: str = "INFO" # External services anthropic_api_key: SecretStr = "" + cohere_api_key: SecretStr = "" deepseek_api_key: SecretStr = "" gemini_api_key: SecretStr = "" openai_api_key: SecretStr = "" openrouter_api_key: SecretStr = "" together_ai_api_key: SecretStr = "" + voyage_api_key: SecretStr = "" + + @property + def cohere_api_key_plain(self) -> str: + return self.cohere_api_key.get_secret_value() + + @property + def voyage_api_key_plain(self) -> str: + return self.voyage_api_key.get_secret_value() @property def anthropic_api_key_plain(self) -> str: diff --git a/python/molto/core.py b/python/molto/core.py index c47e5cc..2847795 100644 --- a/python/molto/core.py +++ b/python/molto/core.py @@ -5,11 +5,20 @@ from pydantic import BaseModel from molto.adapters.anthropic import AnthropicMessagesAdapter -from molto.adapters.openai import OpenAIChatCompletionsAdapter, OpenAIResponsesAdapter +from molto.adapters.embeddings import project_embeddings_response +from molto.adapters.openai import ( + OpenAIChatCompletionsAdapter, + OpenAIResponsesAdapter, + _raise_openai_error, +) from molto.configs import ENV_CONFIG from molto.providers import ( + DEFAULT_EMBEDDING_PROVIDER, DEFAULT_PROVIDER_BY_ENDPOINT, + EMBEDDING_PROVIDERS, PROVIDER_CAPABILITIES, + EmbeddingEndpoint, + EmbeddingProvider, Endpoint, Provider, ) @@ -18,17 +27,28 @@ AnthropicMessagesResponse, AnthropicMessagesStreamEvent, ) +from molto.types.embeddings import CohereEmbeddingOptions, VoyageEmbeddingOptions +from molto.types.exceptions import UnsupportedInput from molto.types.openai_chat_completions import ( OpenAIChatCompletionsCreateRequest, OpenAIChatCompletionsResponse, OpenAIChatCompletionsStreamChunk, ) +from molto.types.openai_embeddings import ( + OpenAIEmbeddingsCreateRequest, + OpenAIEmbeddingsResponse, +) from molto.types.openai_responses import ( OpenAIResponsesCreateRequest, OpenAIResponsesResponse, OpenAIResponsesStreamEvent, ) from molto.upstream.anthropic import AnthropicMessagesClient +from molto.upstream.embeddings import ( + CohereEmbeddingsClient, + OpenAIEmbeddingsClient, + VoyageAIEmbeddingsClient, +) from molto.upstream.openai import OpenAIChatCompletionsClient, OpenAIResponsesClient VLLM_DUMMY_API_KEY = "EMPTY" @@ -163,13 +183,77 @@ async def __aexit__(self, *args: object) -> None: async def aclose(self) -> None: await self._http_client.aclose() + async def embeddings( + self, + body: BaseModel | dict[str, Any], + endpoint: EmbeddingEndpoint = "embeddings", + provider: EmbeddingProvider | None = None, + api_key: str | None = None, + base_url: str | None = None, + extra_body: Mapping[str, Any] | None = None, + extra_headers: Mapping[str, str] | None = None, + *, + options: CohereEmbeddingOptions | VoyageEmbeddingOptions | None = None, + ) -> OpenAIEmbeddingsResponse: + """Create dense embeddings with an OpenAI-shaped request and result. + + `endpoint` selects the wire protocol independently of `provider` and + `base_url`. Cohere and Voyage task/truncation controls use their typed + `options`; native provider metadata is retained in `provider_metadata`. + Unknown token usage is `None`, never an invented zero. See + `docs/provider-embeddings.md` for supported forms and evidence limits. + """ + request = OpenAIEmbeddingsCreateRequest.model_validate(body) + if endpoint not in DEFAULT_EMBEDDING_PROVIDER: + raise UnsupportedInput(f"Unsupported embedding endpoint: {endpoint!r}.") + resolved_provider = provider or DEFAULT_EMBEDDING_PROVIDER[endpoint] + if resolved_provider not in EMBEDDING_PROVIDERS[endpoint]: + raise UnsupportedInput( + f"Provider {resolved_provider!r} does not support embedding endpoint {endpoint!r}." + ) + if resolved_provider in {"infinity", "vllm"} and not base_url: + raise UnsupportedInput( + f"Provider {resolved_provider!r} requires an explicit base_url." + ) + transport = { + "api_key": self._resolve_api_key(resolved_provider, api_key), + "base_url": base_url, + "extra_body": extra_body, + "extra_headers": _upstream_headers(extra_headers), + } + try: + if endpoint == "embeddings": + if options is not None: + raise UnsupportedInput( + "Native embedding options require their matching endpoint." + ) + upstream = OpenAIEmbeddingsClient(self._http_client, **transport) + return await upstream.native_response(request) + if endpoint == "cohere/embed": + if options is not None and not isinstance(options, CohereEmbeddingOptions): + raise UnsupportedInput("Cohere embeddings require CohereEmbeddingOptions.") + upstream = CohereEmbeddingsClient(self._http_client, **transport) + else: + if options is not None and not isinstance(options, VoyageEmbeddingOptions): + raise UnsupportedInput("Voyage embeddings require VoyageEmbeddingOptions.") + upstream = VoyageAIEmbeddingsClient(self._http_client, **transport) + return project_embeddings_response(await upstream.response(request, options)) + except Exception as error: + _raise_openai_error(error) + @staticmethod - def _resolve_api_key(provider: Provider, api_key: str | None) -> str | None: + def _resolve_api_key( + provider: Provider | EmbeddingProvider, api_key: str | None + ) -> str | None: if api_key is None and provider == "openai": return ENV_CONFIG.openai_api_key_plain if api_key is None and provider == "anthropic": return ENV_CONFIG.anthropic_api_key_plain - if api_key is None and provider == "vllm": + if api_key is None and provider == "cohere": + return ENV_CONFIG.cohere_api_key_plain + if api_key is None and provider == "voyageai": + return ENV_CONFIG.voyage_api_key_plain + if api_key is None and provider in {"vllm", "infinity"}: return VLLM_DUMMY_API_KEY return api_key diff --git a/python/molto/providers.py b/python/molto/providers.py index 7e7b2f8..e9bdcac 100644 --- a/python/molto/providers.py +++ b/python/molto/providers.py @@ -3,8 +3,21 @@ Endpoint: TypeAlias = Literal["responses", "chat/completions", "messages"] Provider: TypeAlias = Literal["anthropic", "openai", "vllm"] +EmbeddingEndpoint: TypeAlias = Literal["embeddings", "cohere/embed", "voyage/embeddings"] +EmbeddingProvider: TypeAlias = Literal["openai", "cohere", "voyageai", "infinity", "vllm"] ReasoningRepresentation: TypeAlias = Literal["raw", "summary"] +DEFAULT_EMBEDDING_PROVIDER: dict[EmbeddingEndpoint, EmbeddingProvider] = { + "embeddings": "openai", + "cohere/embed": "cohere", + "voyage/embeddings": "voyageai", +} +EMBEDDING_PROVIDERS: dict[EmbeddingEndpoint, frozenset[EmbeddingProvider]] = { + "embeddings": frozenset({"openai", "infinity", "vllm"}), + "cohere/embed": frozenset({"cohere"}), + "voyage/embeddings": frozenset({"voyageai"}), +} + @dataclass(frozen=True) class ProviderCapabilities: diff --git a/python/molto/types/embeddings.py b/python/molto/types/embeddings.py new file mode 100644 index 0000000..a6e0267 --- /dev/null +++ b/python/molto/types/embeddings.py @@ -0,0 +1,39 @@ +"""Embedding-specific options and normalized dense output. + +Task names and truncation controls remain protocol-specific because their +semantics are not interchangeable between providers. +""" + +from typing import Any, Literal + +from pydantic import StrictBool, StrictFloat, StrictInt, StrictStr + +from molto.types.base import StrictModel + + +class CohereEmbeddingOptions(StrictModel): + input_type: Literal["search_query", "search_document", "classification", "clustering"] + truncate: Literal["NONE", "START", "END"] | None = None + + +class VoyageEmbeddingOptions(StrictModel): + input_type: Literal["query", "document"] | None = None + truncation: StrictBool | None = None + + +class EmbeddingVector(StrictModel): + index: StrictInt + embedding: list[StrictFloat] | StrictStr + provider_metadata: dict[str, Any] = {} + + +class EmbeddingUsage(StrictModel): + prompt_tokens: StrictInt | None = None + total_tokens: StrictInt | None = None + + +class NormalizedEmbeddingResponse(StrictModel): + model: StrictStr + data: list[EmbeddingVector] + usage: EmbeddingUsage | None = None + provider_metadata: dict[str, Any] = {} diff --git a/python/molto/types/openai_embeddings.py b/python/molto/types/openai_embeddings.py new file mode 100644 index 0000000..788c9f7 --- /dev/null +++ b/python/molto/types/openai_embeddings.py @@ -0,0 +1,21 @@ +"""Validated requests and SDK-native results for OpenAI embeddings.""" + +from typing import Literal + +from openai.types import CreateEmbeddingResponse +from pydantic import StrictInt, StrictStr + +from molto.types.base import ExcludeNoneModel + + +class OpenAIEmbeddingsCreateRequest(ExcludeNoneModel): + """The body parameters accepted by `AsyncOpenAI.embeddings.create`.""" + + input: StrictStr | list[StrictStr] | list[StrictInt] | list[list[StrictInt]] + model: StrictStr + dimensions: StrictInt | None = None + encoding_format: Literal["float", "base64"] | None = None + user: StrictStr | None = None + + +OpenAIEmbeddingsResponse = CreateEmbeddingResponse diff --git a/python/molto/upstream/base.py b/python/molto/upstream/base.py index 82d53a4..5fe2700 100644 --- a/python/molto/upstream/base.py +++ b/python/molto/upstream/base.py @@ -16,18 +16,15 @@ ) -class BaseClient[ - NativeRequestT: BaseModel, - NativeEventT, - NativeResponseT: BaseModel, -](ABC): +class BaseHTTPClient: + """Request-scoped transport borrowing the facade's shared HTTP client.""" + DEFAULT_BASE_URL: str def __init__( self, http_client: AsyncClient, *, - capabilities: ProviderCapabilities, api_key: str | None = None, base_url: str | None = None, extra_body: Mapping[str, Any] | None = None, @@ -36,7 +33,6 @@ def __init__( self._http_client = http_client self._api_key = api_key self._base_url = base_url - self._capabilities = capabilities self._extra_body = extra_body self._extra_headers = extra_headers self.response_headers = Headers() @@ -71,6 +67,33 @@ def _make_authentication_request(self, path: str) -> Request: raw_path += b"/" return Request("POST", base_url.copy_with(raw_path=raw_path + path.encode())) + +class BaseClient[ + NativeRequestT: BaseModel, + NativeEventT, + NativeResponseT: BaseModel, +](BaseHTTPClient, ABC): + """Conversational transport and normalized event lifecycle.""" + + def __init__( + self, + http_client: AsyncClient, + *, + capabilities: ProviderCapabilities, + api_key: str | None = None, + base_url: str | None = None, + extra_body: Mapping[str, Any] | None = None, + extra_headers: Mapping[str, str] | None = None, + ) -> None: + super().__init__( + http_client, + api_key=api_key, + base_url=base_url, + extra_body=extra_body, + extra_headers=extra_headers, + ) + self._capabilities = capabilities + def _classify_thinking(self, text: str, *, signature: str | None = None) -> Thinking: """Place a protocol's ambiguous reasoning text in the provider's representation. diff --git a/python/molto/upstream/embeddings.py b/python/molto/upstream/embeddings.py new file mode 100644 index 0000000..c7091e2 --- /dev/null +++ b/python/molto/upstream/embeddings.py @@ -0,0 +1,296 @@ +"""Native OpenAI transport and text-only Cohere/Voyage embedding protocols.""" + +import base64 +import binascii +import sys +from typing import Any, NoReturn + +from openai import APIStatusError, AsyncOpenAI, OpenAIError +from openai import AuthenticationError as OpenAIAuthenticationError +from openai import NotFoundError as OpenAINotFoundError + +from molto.types.embeddings import ( + CohereEmbeddingOptions, + EmbeddingUsage, + EmbeddingVector, + NormalizedEmbeddingResponse, + VoyageEmbeddingOptions, +) +from molto.types.exceptions import ( + AuthenticationError, + ContextLengthExceededError, + ModelNotFoundError, + ResourceNotFoundError, + UnsupportedInput, +) +from molto.types.openai_embeddings import ( + OpenAIEmbeddingsCreateRequest, + OpenAIEmbeddingsResponse, +) +from molto.upstream.base import BaseHTTPClient + + +def _validate_dense_options(body: dict[str, Any]) -> None: + """Reject known alternate representations that are not dense float32 vectors.""" + for field, allowed in ( + ("encoding_format", (None, "float", "base64")), + ("output_dtype", (None, "float")), + ("embed_dtype", (None, "float32")), + ("endianness", (None, "native", sys.byteorder)), + ): + if body.get(field) not in allowed: + raise UnsupportedInput(f"Unsupported embedding {field}: {body[field]!r}.") + if body.get("embedding_types") not in (None, ["float"], ["base64"]): + raise UnsupportedInput("Only one float or base64 embedding representation is supported.") + if body.get("task") == "token_embed": + raise UnsupportedInput("Token embeddings are not one dense vector per input.") + if body.get("stream"): + raise UnsupportedInput("Streaming embeddings are not supported.") + for field in ("messages", "images", "inputs", "late_chunking_params"): + if body.get(field) is not None: + raise UnsupportedInput(f"Embedding {field} is not supported by the text contract.") + + +def _input_count(value: Any) -> int: + if isinstance(value, str) or (isinstance(value, list) and value and type(value[0]) is int): + return 1 + return len(value) + + +def _validate_vectors( + vectors: list[Any], *, count: int, dimensions: int | None, encoding: str | None +) -> None: + """Check correspondence and reject multi-vector output without reordering it.""" + if ( + len(vectors) != count + or any(type(item.index) is not int for item in vectors) + or {item.index for item in vectors} != set(range(count)) + ): + raise ValueError("Embedding response indices do not match the requested inputs.") + for item in vectors: + vector = item.embedding + if isinstance(vector, str): + if encoding != "base64": + raise ValueError("Expected a dense float embedding, received encoded output.") + try: + raw = base64.b64decode(vector, validate=True) + except (ValueError, binascii.Error) as error: + raise ValueError("Invalid base64 embedding.") from error + if len(raw) % 4: + raise ValueError("Base64 embedding is not a float32 vector.") + size = len(raw) // 4 + elif isinstance(vector, list) and all(type(value) in (float, int) for value in vector): + if encoding == "base64": + raise ValueError("Expected a base64 embedding, received float output.") + size = len(vector) + else: + raise ValueError("Embedding output must contain one dense vector per input.") + if dimensions is not None and size != dimensions: + raise ValueError("Embedding output dimensions do not match the request.") + + +class _EmbeddingsClient(BaseHTTPClient): + PATH: str + + def _sdk_client(self) -> AsyncOpenAI: + if not self._api_key: + error = OpenAIError("Missing credentials. Supply an embedding provider API key.") + raise AuthenticationError( + str(error), + response=None, + body=None, + request=self._make_authentication_request(self.PATH.lstrip("/")), + ) from error + return self._make_sdk_client() + + @staticmethod + def _handle_error(error: Exception) -> NoReturn: + if isinstance(error, APIStatusError): + error_type = None + if error.code == "context_length_exceeded": + error_type = ContextLengthExceededError + elif error.code == "model_not_found": + error_type = ModelNotFoundError + elif isinstance(error, OpenAIAuthenticationError): + error_type = AuthenticationError + elif isinstance(error, OpenAINotFoundError): + error_type = ResourceNotFoundError + if error_type is not None: + raise error_type( + error.message, response=error.response, body=error.body + ) from error + raise error + + async def _post(self, body: dict[str, Any]) -> dict[str, Any]: + try: + return await self._sdk_client().post( + self.PATH, + body=body, + cast_to=dict[str, Any], + options={"headers": self._extra_headers or {}}, + ) + except Exception as error: + self._handle_error(error) + + +class OpenAIEmbeddingsClient(_EmbeddingsClient): + """Reuse the SDK's omitted-encoding decoder and native response models.""" + + DEFAULT_BASE_URL = "https://api.openai.com/v1" + PATH = "/embeddings" + + async def native_response( + self, request: OpenAIEmbeddingsCreateRequest + ) -> OpenAIEmbeddingsResponse: + body = request.model_dump() + effective_body = {**body, **(self._extra_body or {})} + _validate_dense_options(effective_body) + effective_request = OpenAIEmbeddingsCreateRequest.model_validate( + { + key: effective_body[key] + for key in OpenAIEmbeddingsCreateRequest.model_fields + if key in effective_body + } + ) + try: + response = await self._sdk_client().embeddings.create( + **body, extra_body=self._extra_body, extra_headers=self._extra_headers + ) + except Exception as error: + self._handle_error(error) + # The SDK decodes omitted encoding on the caller's behalf. Validate the + # parsed representation, preserving the native SDK object and extensions. + _validate_vectors( + response.data, + count=_input_count(effective_request.input), + dimensions=effective_request.dimensions, + encoding=( + effective_request.encoding_format if request.encoding_format is not None else None + ), + ) + return response + + +def _text_input(request: OpenAIEmbeddingsCreateRequest) -> str | list[str]: + value = request.input + if not isinstance(value, str) and not ( + isinstance(value, list) and all(isinstance(item, str) for item in value) + ): + raise UnsupportedInput("This embedding protocol accepts text, not token IDs.") + if request.user is not None: + raise UnsupportedInput("This embedding protocol cannot preserve the OpenAI user field.") + return value + + +class CohereEmbeddingsClient(_EmbeddingsClient): + DEFAULT_BASE_URL = "https://api.cohere.com/v2" + PATH = "/embed" + + async def response( + self, request: OpenAIEmbeddingsCreateRequest, options: CohereEmbeddingOptions | None + ) -> NormalizedEmbeddingResponse: + value = _text_input(request) + if options is None: + raise UnsupportedInput("Cohere requires explicit CohereEmbeddingOptions.input_type.") + body: dict[str, Any] = { + "model": request.model, + "texts": [value] if isinstance(value, str) else value, + "embedding_types": [request.encoding_format or "float"], + **options.model_dump(), + } + if request.dimensions is not None: + body["output_dimension"] = request.dimensions + body.update(self._extra_body or {}) + _validate_dense_options(body) + texts = body["texts"] + if not isinstance(texts, list) or not all(isinstance(text, str) for text in texts): + raise UnsupportedInput("Cohere texts must remain a list of strings.") + encoding = (body.get("embedding_types") or ["float"])[0] + result = await self._post(body) + representations = {"float", "base64", "int8", "uint8", "binary", "ubinary"} + if any( + key in representations and key != encoding and value is not None + for key, value in result["embeddings"].items() + ): + raise ValueError("Cohere returned multiple embedding representations.") + data = [ + EmbeddingVector(index=index, embedding=vector) + for index, vector in enumerate(result["embeddings"][encoding]) + ] + _validate_vectors( + data, count=len(texts), dimensions=body.get("output_dimension"), encoding=encoding + ) + # Billed units describe billing, not actual prompt/total token usage. + metadata = {key: value for key, value in result.items() if key != "embeddings"} + embedding_metadata = { + key: value for key, value in result["embeddings"].items() if key != encoding + } + if embedding_metadata: + metadata["embeddings"] = embedding_metadata + return NormalizedEmbeddingResponse( + model=body["model"], + data=data, + provider_metadata=metadata, + ) + + +class VoyageAIEmbeddingsClient(_EmbeddingsClient): + DEFAULT_BASE_URL = "https://api.voyageai.com/v1" + PATH = "/embeddings" + + async def response( + self, request: OpenAIEmbeddingsCreateRequest, options: VoyageEmbeddingOptions | None + ) -> NormalizedEmbeddingResponse: + body: dict[str, Any] = { + "model": request.model, + "input": _text_input(request), + "output_dtype": "float", + **(options.model_dump() if options is not None else {}), + } + if request.dimensions is not None: + body["output_dimension"] = request.dimensions + # Voyage uses an omitted/null encoding for JSON floats, not "float". + if request.encoding_format == "base64": + body["encoding_format"] = "base64" + body.update(self._extra_body or {}) + _validate_dense_options(body) + if body.get("encoding_format") == "float": + raise UnsupportedInput("Voyage encoding_format uses null for float output.") + value = body["input"] + if not isinstance(value, str) and not ( + isinstance(value, list) and all(isinstance(item, str) for item in value) + ): + raise UnsupportedInput("Voyage input must remain text or a list of texts.") + result = await self._post(body) + data = [ + EmbeddingVector( + index=item["index"], + embedding=item["embedding"], + provider_metadata={ + key: value + for key, value in item.items() + if key not in {"index", "embedding", "object"} + }, + ) + for item in result["data"] + ] + _validate_vectors( + data, + count=_input_count(value), + dimensions=body.get("output_dimension"), + encoding=body.get("encoding_format"), + ) + usage = result.get("usage") + return NormalizedEmbeddingResponse( + model=result.get("model", body["model"]), + data=data, + usage=( + EmbeddingUsage( + prompt_tokens=usage.get("prompt_tokens"), + total_tokens=usage.get("total_tokens"), + ) + if usage is not None + else None + ), + provider_metadata={key: value for key, value in result.items() if key != "data"}, + ) diff --git a/tests/cassettes/README.md b/tests/cassettes/README.md index f3b4d83..a945824 100644 --- a/tests/cassettes/README.md +++ b/tests/cassettes/README.md @@ -4,6 +4,14 @@ These YAML files capture sanitized HTTP requests and provider responses for dete Normal test runs are playback-only. Do not record or rewrite cassettes during a test run. +## Embedding evidence + +`embeddings/recorded/{openai,cohere,voyageai}/` contains independent native HTTP captures made outside pytest on 2026-11-03. Each manifest identifies the driver, API/SDK version or schema reference, request parameters, status, and fixture hashes. Authentication is masked and headers are whitelisted; `response.body_bytes` retains the decoded original HTTP response bytes. Tests replay those bytes and compare exact request bodies. No vector values are matched across independently generated models. + +OpenAI captures include text and token-ID inputs, single inputs and batches, omitted/float/base64 encoding, explicit/default dimensions, and real request errors. Public native replay compares against the parsed pinned OpenAI SDK on the same evidence. Cohere v2 captures cover query/document/classification/clustering tasks, dimensions, float/base64, and a missing-model error. The VoyageAI captures are real authentication failures, not successful embeddings. + +`embeddings/synthetic/` contains explicitly hand-authored schema fixtures. Infinity and vLLM manifests pin the reviewed server source versions; these fixtures establish local request/response and SDK parity, not live server/model compatibility. VoyageAI synthetic success cases likewise do not substitute for a successful live capture. The [support matrix](../../docs/provider-embeddings.md) records the remaining access and deployment gaps. Existing conversational cassettes are unchanged. + ## Recorded cassettes The cassettes under provider directories such as `openai/` preserve the recorded request and response as closely as possible. Authorization values and secret-looking request fields are masked, and only selected headers are retained so the fixtures remain safe and deterministic. diff --git a/tests/cassettes/embeddings/synthetic/infinity/validation-error.json b/tests/cassettes/embeddings/synthetic/infinity/validation-error.json index 93967b2..b0b827f 100644 --- a/tests/cassettes/embeddings/synthetic/infinity/validation-error.json +++ b/tests/cassettes/embeddings/synthetic/infinity/validation-error.json @@ -16,7 +16,7 @@ "body": { "message": "Batch is empty", "code": 400, - "error_type": "empty" + "error_type": "Empty" } } } diff --git a/tests/core/test_embedding_engines.py b/tests/core/test_embedding_engines.py new file mode 100644 index 0000000..dc26583 --- /dev/null +++ b/tests/core/test_embedding_engines.py @@ -0,0 +1,177 @@ +"""Schema-based engine replay; these fixtures are not live engine recordings.""" + +import json +from copy import deepcopy +from pathlib import Path + +import pytest +from httpx import AsyncClient, MockTransport, Request, Response +from openai import AsyncOpenAI, BadRequestError +from pydantic import ValidationError + +from molto import Molto +from molto.types.exceptions import UnsupportedInput + +FIXTURES = Path(__file__).parents[1] / "cassettes" / "embeddings" / "synthetic" +ENGINES = ("infinity", "vllm") +CASES = [ + (engine, case) + for engine in ENGINES + for case in json.loads((FIXTURES / engine / "manifest.json").read_text())["cases"] +] + + +@pytest.mark.parametrize("engine,case", CASES, ids=[f"{e}-{c['file']}" for e, c in CASES]) +@pytest.mark.parametrize("api_key", [None, "engine-test-key"]) +async def test_engine_sdk_parity(engine, case, api_key): + """Ensures engine requests and parsed responses match the SDK for each input form.""" + cassette = json.loads((FIXTURES / engine / case["file"]).read_text()) + requests = [] + resolved_key = api_key or "EMPTY" + + def handle(request: Request) -> Response: + requests.append(request) + assert request.method == cassette["request"]["method"] + assert str(request.url) == "https://engine.example/v1/embeddings" + assert request.headers["authorization"] == f"Bearer {resolved_key}" + assert json.loads(request.content) == cassette["request"]["body"] + response = cassette["response"] + return Response( + response["status_code"], headers=response["headers"], json=response["body"] + ) + + async with AsyncClient(transport=MockTransport(handle)) as reference_http: + sdk = AsyncOpenAI( + api_key=resolved_key, + base_url="https://engine.example/v1", + http_client=reference_http, + max_retries=0, + ) + if cassette["response"]["status_code"] >= 400: + with pytest.raises(BadRequestError) as reference_error: + await sdk.embeddings.create(**case["body"], extra_body=case["extra_body"]) + else: + expected = await sdk.embeddings.create(**case["body"], extra_body=case["extra_body"]) + + original_body = deepcopy(case["body"]) + async with Molto(http_client=AsyncClient(transport=MockTransport(handle))) as molto: + kwargs = dict( + provider=engine, + api_key=api_key, + base_url="https://engine.example/v1", + extra_body=case["extra_body"], + ) + if cassette["response"]["status_code"] >= 400: + with pytest.raises(BadRequestError) as actual_error: + await molto.embeddings(case["body"], **kwargs) + assert actual_error.value.status_code == reference_error.value.status_code + assert actual_error.value.body == reference_error.value.body + assert actual_error.value.response.content == reference_error.value.response.content + else: + actual = await molto.embeddings(case["body"], **kwargs) + assert type(actual) is type(expected) + assert actual.model_dump() == expected.model_dump() + assert [item.index for item in actual.data] == list(range(len(actual.data))) + assert case["body"] == original_body + assert len(requests) == 2 + + +@pytest.mark.parametrize("engine", ENGINES) +async def test_engine_requires_explicit_destination(engine): + """Ensures an engine credential cannot be sent to a hosted default destination.""" + + def unexpected_request(request: Request) -> Response: + pytest.fail("Missing engine URL must fail before HTTP") + + async with Molto( + http_client=AsyncClient(transport=MockTransport(unexpected_request)) + ) as molto: + with pytest.raises(ValueError, match="base_url"): + await molto.embeddings({"model": "test", "input": "text"}, provider=engine) + + +@pytest.mark.parametrize("engine", ENGINES) +@pytest.mark.parametrize( + "extra_body", + [ + {"encoding_format": "tensor"}, + {"encoding_format": "bytes"}, + {"encoding_format": "bytes_only"}, + {"embed_dtype": "float16"}, + {"endianness": "big"}, + {"messages": [{"role": "user", "content": "text"}]}, + {"late_chunking_params": {"chunk_size": 2}}, + {"input": [{"role": "user", "content": "text"}]}, + ], +) +async def test_engine_rejects_non_dense_protocol_modes(engine, extra_body): + """Ensures unsupported tensor, multimodal and contextual modes fail without HTTP.""" + + def unexpected_request(request: Request) -> Response: + pytest.fail("Unsupported modes must fail before HTTP") + + async with Molto( + http_client=AsyncClient(transport=MockTransport(unexpected_request)) + ) as molto: + with pytest.raises((UnsupportedInput, ValidationError)): + await molto.embeddings( + {"model": "test", "input": "text"}, + provider=engine, + base_url="https://engine.example/v1", + extra_body=extra_body, + ) + + +@pytest.mark.parametrize("engine", ENGINES) +@pytest.mark.parametrize( + "embedding", + [ + [[0.25, 0.5], [0.75, 1.0]], + {"shape": [2], "dtype": "float32", "data_encoding": "base64", "data": "AACAPwAAAEA="}, + {"indices": [1, 3], "values": [0.25, 0.75]}, + ], +) +async def test_engine_rejects_non_dense_vectors(engine, embedding): + """Ensures server token matrices, tensors and sparse data are never flattened to dense vectors.""" + + def handle(request: Request) -> Response: + return Response( + 200, + json={ + "object": "list", + "model": "test", + "data": [{"index": 0, "object": "embedding", "embedding": embedding}], + "usage": {"prompt_tokens": 1, "total_tokens": 1}, + }, + ) + + async with Molto(http_client=AsyncClient(transport=MockTransport(handle))) as molto: + with pytest.raises(ValueError): + await molto.embeddings( + {"model": "test", "input": "text", "encoding_format": "float"}, + provider=engine, + base_url="https://engine.example/v1", + ) + + +@pytest.mark.parametrize("engine", ENGINES) +async def test_engine_preserves_response_order_and_indices(engine): + """Ensures reordered native results retain their indices and extensions without reassociation.""" + cassette = json.loads((FIXTURES / engine / "batch-float.json").read_text()) + response_body = cassette["response"]["body"] + response_body["data"].reverse() + response_body["server_extension"] = {"revision": "test"} + response_body["data"][0]["item_extension"] = "retained" + + def handle(request: Request) -> Response: + assert json.loads(request.content) == cassette["request"]["body"] + return Response(200, json=response_body) + + async with Molto(http_client=AsyncClient(transport=MockTransport(handle))) as molto: + result = await molto.embeddings( + cassette["request"]["body"], + provider=engine, + base_url="https://engine.example/v1", + ) + assert [item.index for item in result.data] == [1, 0] + assert result.model_dump() == response_body diff --git a/tests/core/test_embeddings.py b/tests/core/test_embeddings.py new file mode 100644 index 0000000..ca18e9b --- /dev/null +++ b/tests/core/test_embeddings.py @@ -0,0 +1,380 @@ +"""Public embeddings parity against independently recorded native SDK requests.""" + +import asyncio +import hashlib +import inspect +import json +from copy import deepcopy +from pathlib import Path +from typing import Any + +import pytest +from httpx import AsyncClient, MockTransport, Request, Response +from openai import APIStatusError, AsyncOpenAI, AuthenticationError +from openai.resources.embeddings import AsyncEmbeddings +from openai.types import CreateEmbeddingResponse +from pydantic import SecretStr, ValidationError + +from molto import Molto +from molto.configs import ENV_CONFIG +from molto.types.embeddings import CohereEmbeddingOptions +from molto.types.openai_embeddings import ( + OpenAIEmbeddingsCreateRequest, + OpenAIEmbeddingsResponse, +) +from molto.utils.io import read_yaml + +CASSETTE_DIR = Path(__file__).parents[1] / "cassettes/embeddings/recorded/openai" +MANIFEST = read_yaml(CASSETTE_DIR / "manifest.yaml") +SUCCESS_CASES = [case for case in MANIFEST["scenarios"] if case["result"] == "success"] +ERROR_CASES = [case for case in MANIFEST["scenarios"] if case["result"] == "upstream_error"] + + +def _recording(name: str) -> dict[str, Any]: + return read_yaml(CASSETTE_DIR / f"{name}.yaml") + + +def _client(cassette: dict[str, Any]) -> tuple[AsyncClient, list[Request]]: + requests = [] + + def replay(request: Request) -> Response: + requests.append(request) + assert request.method == cassette["request"]["method"] + assert request.url.path == cassette["request"]["path"] + assert json.loads(request.content) == cassette["request"]["body"] + recorded = cassette["response"] + return Response( + recorded["status_code"], + headers=recorded["headers"], + content=recorded["body_bytes"], + request=request, + ) + + return AsyncClient(transport=MockTransport(replay)), requests + + +@pytest.mark.parametrize("case", MANIFEST["scenarios"], ids=lambda case: case["scenario"]) +def test_recorded_embedding_evidence_integrity(case: dict[str, Any]) -> None: + """Ensures promoted HTTP bytes still match the independent recording manifest.""" + source = (CASSETTE_DIR / case["file"]).read_bytes() + assert hashlib.sha256(source).hexdigest() == case["sha256"] + cassette = read_yaml(CASSETTE_DIR / case["file"]) + assert json.loads(cassette["response"]["body_bytes"]) == cassette["response"]["body"] + + +@pytest.mark.parametrize("case", SUCCESS_CASES, ids=lambda case: case["scenario"]) +async def test_embeddings_native_recorded_sdk_parity(case: dict[str, Any]) -> None: + """Ensures exact requests and parsed SDK vectors, indices, models, and usage survive.""" + cassette = read_yaml(CASSETTE_DIR / case["file"]) + body = deepcopy(case["sdk_arguments"]) + reference_client, reference_requests = _client(cassette) + async with reference_client: + reference = await AsyncOpenAI( + api_key="reference-key", http_client=reference_client, max_retries=0 + ).embeddings.create(**body) + + http_client, requests = _client(cassette) + async with Molto(http_client=http_client) as molto: + actual = await molto.embeddings(body, api_key="molto-key") + assert not http_client.is_closed + assert http_client.is_closed + assert isinstance(actual, CreateEmbeddingResponse) + assert actual.model_dump(mode="python", warnings=False) == reference.model_dump( + mode="python", warnings=False + ) + assert body == case["sdk_arguments"] + assert len(requests) == len(reference_requests) == 1 + assert requests[0].url == reference_requests[0].url + assert actual.model == case["returned_model"] + assert [row.index for row in actual.data] == case["indices"] + expected_type = str if body.get("encoding_format") == "base64" else list + assert all(isinstance(row.embedding, expected_type) for row in actual.data) + if expected_type is list: + assert all(len(row.embedding) == body.get("dimensions", 1536) for row in actual.data) + + +@pytest.mark.parametrize("case", ERROR_CASES, ids=lambda case: case["scenario"]) +async def test_embeddings_recorded_error_sdk_parity(case: dict[str, Any]) -> None: + """Ensures native provider failures keep the SDK exception and original response.""" + cassette = read_yaml(CASSETTE_DIR / case["file"]) + reference_client, _ = _client(cassette) + async with reference_client: + with pytest.raises(APIStatusError) as reference: + await AsyncOpenAI( + api_key="reference-key", http_client=reference_client, max_retries=0 + ).embeddings.create(**case["sdk_arguments"]) + + http_client, requests = _client(cassette) + async with Molto(http_client=http_client) as molto: + with pytest.raises(APIStatusError) as actual: + await molto.embeddings(case["sdk_arguments"], api_key="molto-key") + assert len(requests) == 1 + assert type(actual.value) is type(reference.value) + assert actual.value.status_code == reference.value.status_code + assert actual.value.body == reference.value.body + assert actual.value.message == reference.value.message + assert actual.value.response.content == reference.value.response.content + + +def test_embeddings_public_schema_tracks_sdk() -> None: + """Ensures the public request fields and native response alias match the SDK contract.""" + parameters = set(inspect.signature(AsyncEmbeddings.create).parameters) + transport = {"self", "extra_headers", "extra_query", "extra_body", "timeout"} + assert set(OpenAIEmbeddingsCreateRequest.model_fields) == parameters - transport + assert OpenAIEmbeddingsResponse is CreateEmbeddingResponse + request = OpenAIEmbeddingsCreateRequest(model="example", input="text") + assert request.model_dump() == {"model": "example", "input": "text"} + + +@pytest.mark.parametrize( + "input_value", + [True, 1, 1.5, [True], [1.5], [[False]], [[1.5]], [1, "text"], {"text": "value"}], +) +async def test_embeddings_rejects_coerced_inputs_before_http(input_value: object) -> None: + """Ensures invalid token and text forms cannot be silently coerced into different inputs.""" + calls = [] + + def unexpected(request: Request) -> Response: + calls.append(request) + raise AssertionError("Invalid input reached HTTP") + + async with Molto(http_client=AsyncClient(transport=MockTransport(unexpected))) as molto: + with pytest.raises(ValidationError): + await molto.embeddings({"model": "example", "input": input_value}, api_key="key") + assert calls == [] + + +@pytest.mark.parametrize("input_value", [[True], [1.5], [1, "text"], {"text": "value"}]) +async def test_embeddings_rejects_invalid_extra_body_inputs_before_http( + input_value: object, +) -> None: + """Ensures transport overrides cannot bypass strict input semantics with a valid typed body.""" + async with Molto( + http_client=AsyncClient(transport=MockTransport(lambda _: pytest.fail())) + ) as molto: + with pytest.raises(ValidationError): + await molto.embeddings( + {"model": "example", "input": "valid text"}, + api_key="key", + extra_body={"input": input_value}, + ) + + +@pytest.mark.parametrize("field", ["stream", "input_type", "truncate", "title", "unknown"]) +async def test_embeddings_rejects_unmodeled_body_fields(field: str) -> None: + """Ensures embedding options cannot disappear through a permissive request model.""" + async with Molto( + http_client=AsyncClient(transport=MockTransport(lambda _: pytest.fail())) + ) as molto: + with pytest.raises(ValidationError): + await molto.embeddings({"model": "example", "input": "text", field: True}) + + +@pytest.mark.parametrize( + ("endpoint", "provider"), + [ + ("chat/completions", "openai"), + ("embeddings", "cohere"), + ("embeddings", "voyageai"), + ("cohere/embed", "openai"), + ("voyage/embeddings", "openai"), + ("embeddings", "unknown"), + ], +) +async def test_embeddings_rejects_incompatible_routes_before_http( + endpoint: str, provider: str +) -> None: + """Ensures provider selection never silently switches the declared wire protocol.""" + async with Molto( + http_client=AsyncClient(transport=MockTransport(lambda _: pytest.fail())) + ) as molto: + with pytest.raises(ValueError): + await molto.embeddings( + {"model": "example", "input": "text"}, + endpoint=endpoint, + provider=provider, + api_key="key", + ) + + +@pytest.mark.parametrize( + ("name", "encoding", "override"), + [ + ("batch-float", "base64", "float"), + ("batch-base64", "float", "base64"), + ("batch-base64", None, "base64"), + ], +) +async def test_embeddings_extra_body_retains_sdk_encoding_precedence( + name: str, encoding: str | None, override: str +) -> None: + """Ensures wire overrides and the SDK's explicit-versus-omitted decoding remain identical.""" + cassette = _recording(name) + arguments = deepcopy(cassette["request"]["body"]) + arguments.pop("encoding_format") + arguments["input"] = "This input is deliberately overridden." + arguments["model"] = "deliberately-overridden-model" + arguments["dimensions"] = 64 + if encoding is not None: + arguments["encoding_format"] = encoding + extra_body = { + field: cassette["request"]["body"][field] for field in ("input", "model", "dimensions") + } + extra_body["encoding_format"] = override + reference_client, _ = _client(cassette) + async with reference_client: + reference = await AsyncOpenAI( + api_key="key", http_client=reference_client, max_retries=0 + ).embeddings.create(**arguments, extra_body=extra_body) + http_client, _ = _client(cassette) + async with Molto(http_client=http_client) as molto: + actual = await molto.embeddings(arguments, api_key="key", extra_body=extra_body) + assert actual.model_dump(mode="python", warnings=False) == reference.model_dump( + mode="python", warnings=False + ) + + +async def test_embeddings_rejects_native_provider_options_on_openai_route() -> None: + """Ensures a provider task option cannot be accepted on an incompatible native route.""" + async with Molto( + http_client=AsyncClient(transport=MockTransport(lambda _: pytest.fail())) + ) as molto: + with pytest.raises(ValueError, match="options"): + await molto.embeddings( + {"model": "example", "input": "text"}, + api_key="key", + options=CohereEmbeddingOptions(input_type="search_query"), + ) + + +async def test_embeddings_extra_body_token_tuple_is_one_input() -> None: + """Ensures accepted token sequences remain one input when serialized by the SDK.""" + cassette = _recording("token-array-float") + body = {**cassette["request"]["body"], "input": "overridden text"} + extra_body = {"input": tuple(cassette["request"]["body"]["input"])} + reference_client, _ = _client(cassette) + async with reference_client: + reference = await AsyncOpenAI( + api_key="key", http_client=reference_client, max_retries=0 + ).embeddings.create(**body, extra_body=extra_body) + http_client, _ = _client(cassette) + async with Molto(http_client=http_client) as molto: + actual = await molto.embeddings(body, api_key="key", extra_body=extra_body) + assert actual.model_dump() == reference.model_dump() + assert len(actual.data) == 1 + + +async def test_embeddings_native_preserves_reordered_results_and_extensions() -> None: + """Ensures a synthetic reordered native response retains input indices and opaque metadata.""" + cassette = _recording("batch-float") + payload = deepcopy(cassette["response"]["body"]) + payload["data"].reverse() + payload["data"][0]["provider_metadata"] = {"source": "second input"} + payload["usage"]["billable_units"] = 7 + payload["provider_request_id"] = "synthetic-request" + cassette["response"]["body_bytes"] = json.dumps(payload).encode() + reference_client, _ = _client(cassette) + async with reference_client: + reference = await AsyncOpenAI( + api_key="key", http_client=reference_client, max_retries=0 + ).embeddings.create(**cassette["request"]["body"]) + http_client, _ = _client(cassette) + async with Molto(http_client=http_client) as molto: + actual = await molto.embeddings(cassette["request"]["body"], api_key="key") + assert actual.model_dump() == reference.model_dump() + assert [item.index for item in actual.data] == [1, 0] + assert actual.data[0].model_extra == {"provider_metadata": {"source": "second input"}} + assert actual.usage.model_extra == {"billable_units": 7} + + +async def test_embeddings_credentials_headers_base_url_and_shared_lifecycle(monkeypatch) -> None: + """Ensures requests reuse Molto's client while credentials, URL, timeout, and extras stay scoped.""" + monkeypatch.setattr(ENV_CONFIG, "openai_api_key", SecretStr("configured-key")) + monkeypatch.setenv("OPENAI_API_KEY", "unrelated-environment-key") + monkeypatch.setenv("OPENAI_BASE_URL", "https://wrong.invalid/") + monkeypatch.setenv("OPENAI_ORG_ID", "unrelated-organization") + monkeypatch.setenv("OPENAI_PROJECT_ID", "unrelated-project") + cassette = _recording("batch-float") + requests = [] + + def replay(request: Request) -> Response: + requests.append(request) + assert request.extensions["timeout"] == { + name: 7.5 for name in ("connect", "read", "write", "pool") + } + assert request.url == "https://example.test/custom/v1/embeddings" + assert "openai-organization" not in request.headers + assert "openai-project" not in request.headers + for excluded in ("x-api-key", "cookie", "x-connection-token", "x-forwarded-for"): + assert excluded not in request.headers + return Response(200, json=cassette["response"]["body"], request=request) + + http_client = AsyncClient(transport=MockTransport(replay), timeout=7.5) + async with Molto(http_client=http_client) as molto: + await molto.embeddings( + cassette["request"]["body"], + base_url="https://example.test/custom/v1", + extra_headers={ + "Authorization": "Bearer client-secret", + "X-Api-Key": "client-secret", + "Cookie": "session=client-secret", + "Connection": "X-Connection-Token", + "X-Connection-Token": "client-secret", + "OpenAI-Organization": "client-organization", + "X-Forwarded-For": "192.0.2.1", + "X-Application": "first-call", + }, + ) + assert not http_client.is_closed + await molto.embeddings( + cassette["request"]["body"], + api_key="explicit-key", + base_url="https://example.test/custom/v1/", + ) + assert not http_client.is_closed + assert http_client.is_closed + assert len(requests) == 2 + assert requests[0].headers["authorization"] == "Bearer configured-key" + assert requests[0].headers["x-application"] == "first-call" + assert requests[1].headers["authorization"] == "Bearer explicit-key" + assert "x-application" not in requests[1].headers + + +async def test_embeddings_missing_credentials_do_not_fall_back_to_sdk_environment( + monkeypatch, +) -> None: + """Ensures a missing resolved key fails locally even when the SDK environment contains a key.""" + monkeypatch.setattr(ENV_CONFIG, "openai_api_key", SecretStr("")) + monkeypatch.setenv("OPENAI_API_KEY", "unrelated-environment-key") + async with Molto( + http_client=AsyncClient(transport=MockTransport(lambda _: pytest.fail())) + ) as molto: + with pytest.raises(AuthenticationError): + await molto.embeddings({"model": "example", "input": "text"}) + + +async def test_embeddings_cancellation_does_not_close_shared_client() -> None: + """Ensures cancelling one in-flight embedding request releases execution but preserves the client.""" + started = asyncio.Event() + cancelled = asyncio.Event() + + async def wait_for_cancellation(request: Request) -> Response: + started.set() + try: + await asyncio.Event().wait() + finally: + cancelled.set() + raise AssertionError("Unreachable") + + http_client = AsyncClient(transport=MockTransport(wait_for_cancellation)) + async with Molto(http_client=http_client) as molto: + task = asyncio.create_task( + molto.embeddings({"model": "example", "input": "text"}, api_key="key") + ) + await asyncio.wait_for(started.wait(), timeout=5) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert cancelled.is_set() + assert not http_client.is_closed + assert http_client.is_closed diff --git a/tests/test_embedding_translation.py b/tests/test_embedding_translation.py new file mode 100644 index 0000000..cf460fb --- /dev/null +++ b/tests/test_embedding_translation.py @@ -0,0 +1,399 @@ +"""Public embedding translation against faithful recordings and explicit synthetic cases.""" + +import hashlib +import json +from pathlib import Path + +import httpx +import pytest +import yaml +from openai import AuthenticationError, NotFoundError +from pydantic import SecretStr + +from molto import Molto +from molto.configs import ENV_CONFIG +from molto.types.embeddings import CohereEmbeddingOptions, VoyageEmbeddingOptions +from molto.types.exceptions import APIException, UnsupportedInput + +RECORDED = Path(__file__).parent / "cassettes" / "embeddings" / "recorded" + + +def _recording(provider, scenario): + return yaml.safe_load((RECORDED / provider / f"{scenario}.yaml").read_text()) + + +def _replay(recording, seen): + def respond(request): + seen.append(request) + assert str(request.url) == recording["request"]["url"] + assert json.loads(request.content) == recording["request"]["body"] + assert request.headers["authorization"] == "Bearer test-key" + return httpx.Response( + recording["response"]["status_code"], + headers=recording["response"]["headers"], + content=recording["response"]["body_bytes"], + ) + + return httpx.MockTransport(respond) + + +@pytest.mark.parametrize("provider", ["cohere", "voyageai"]) +def test_native_recordings_unchanged(provider): + """Ensures replay evidence remains byte-for-byte identical to its reviewed manifest.""" + manifest = yaml.safe_load((RECORDED / provider / "manifest.yaml").read_text()) + for entry in manifest["fixtures"]: + path = RECORDED / provider / entry["file"] + assert hashlib.sha256(path.read_bytes()).hexdigest() == entry["sha256"] + record = yaml.safe_load(path.read_text()) + assert json.loads(record["response"]["body_bytes"]) == record["response"]["body"] + + +@pytest.mark.parametrize( + ("scenario", "encoding"), + [ + (scenario, encoding) + for scenario in ["single-query", "batch-document", "classification", "clustering"] + for encoding in [None, "float"] + ] + + [("batch-base64", "base64")], +) +async def test_cohere_recorded_public_translation(scenario, encoding): + """Ensures Cohere's native v2 schema preserves task, vectors, order, dimensions, and metadata.""" + cassette = _recording("cohere", scenario) + wire = cassette["request"]["body"] + actual_encoding = wire["embedding_types"][0] + body = { + "model": wire["model"], + "input": wire["texts"][0] if scenario == "single-query" else wire["texts"], + "dimensions": wire["output_dimension"], + } + if encoding is not None: + body["encoding_format"] = encoding + seen = [] + client = httpx.AsyncClient(transport=_replay(cassette, seen)) + async with Molto(client) as molto: + result = await molto.embeddings( + body, + endpoint="cohere/embed", + api_key="test-key", + options=CohereEmbeddingOptions( + input_type=wire["input_type"], truncate=wire["truncate"] + ), + ) + assert not client.is_closed + assert client.is_closed + native = cassette["response"]["body"] + assert result.object == "list" + assert result.model == wire["model"] + assert [item.index for item in result.data] == list(range(len(wire["texts"]))) + assert [item.embedding for item in result.data] == native["embeddings"][actual_encoding] + assert result.usage is None + assert result.provider_metadata == { + key: value for key, value in native.items() if key != "embeddings" + } + assert len(seen) == 1 + + +@pytest.mark.parametrize( + ("provider", "scenario", "error_type"), + [("cohere", "invalid-model", NotFoundError)] + + [ + ("voyageai", name, AuthenticationError) + for name in ( + "single-default", + "single-query", + "batch-document", + "batch-base64", + "invalid-model", + ) + ], +) +async def test_recorded_native_errors(provider, scenario, error_type): + """Ensures actual Cohere 404 and Voyage 401 bodies survive the shared error cause chain.""" + cassette = _recording(provider, scenario) + wire = cassette["request"]["body"] + cohere = provider == "cohere" + body = {"model": wire["model"], "input": wire["texts"] if cohere else wire["input"]} + if "output_dimension" in wire: + body["dimensions"] = wire["output_dimension"] + if "encoding_format" in wire: + body["encoding_format"] = wire["encoding_format"] + options = ( + CohereEmbeddingOptions(input_type=wire["input_type"]) + if cohere + else VoyageEmbeddingOptions.model_validate( + {key: wire[key] for key in ("input_type", "truncation") if key in wire} + ) + ) + seen = [] + async with Molto(httpx.AsyncClient(transport=_replay(cassette, seen))) as molto: + with pytest.raises(error_type) as caught: + await molto.embeddings( + body, + endpoint="cohere/embed" if cohere else "voyage/embeddings", + api_key="test-key", + options=options, + ) + assert caught.value.response.json() == cassette["response"]["body"] + assert isinstance(caught.value.__cause__, APIException) + assert len(seen) == 1 + + +@pytest.mark.parametrize("encoding", [None, "float", "base64"]) +@pytest.mark.parametrize("input_type", [None, "query", "document"]) +async def test_voyage_schema_translation_synthetic(encoding, input_type): + """Ensures Voyage's documented schema preserves reordered vectors without invented prompt usage. + + This is deliberate synthetic schema coverage, not a successful live Voyage + capture: https://docs.voyageai.com/reference/embeddings-api. + """ + name = f"{input_type or 'default'}-{'base64' if encoding == 'base64' else 'float'}" + fixture = yaml.safe_load( + (RECORDED.parent / "synthetic" / "voyageai" / f"{name}.yaml").read_text() + ) + native = fixture["response"]["body"] + expected = fixture["request"]["body"] + seen = [] + + def respond(request): + seen.append(request) + assert request.url == "https://example.test/prefix/embeddings" + assert json.loads(request.content) == expected + assert request.headers["authorization"] == "Bearer test-key" + assert request.headers["x-request-note"] == "trace" + assert "openai-organization" not in request.headers + return httpx.Response(200, json=native) + + body = { + "model": expected["model"], + "input": expected["input"], + "dimensions": expected["output_dimension"], + } + if encoding is not None: + body["encoding_format"] = encoding + async with Molto(httpx.AsyncClient(transport=httpx.MockTransport(respond))) as molto: + result = await molto.embeddings( + body, + endpoint="voyage/embeddings", + api_key="test-key", + base_url="https://example.test/prefix", + options=VoyageEmbeddingOptions(input_type=input_type, truncation=False), + extra_headers={ + "Authorization": "unsafe", + "OpenAI-Organization": "unsafe", + "x-request-note": "trace", + }, + ) + assert [item.index for item in result.data] == [1, 0] + assert [item.embedding for item in result.data] == [ + item["embedding"] for item in native["data"] + ] + assert [item.provider_metadata for item in result.data] == [{"trace": 1}, {"trace": 0}] + assert result.usage.prompt_tokens is None + assert result.usage.total_tokens == 7 + assert result.provider_metadata == { + key: value for key, value in native.items() if key != "data" + } + assert len(seen) == 1 + + +@pytest.mark.parametrize( + "endpoint,field", [("cohere/embed", "cohere_api_key"), ("voyage/embeddings", "voyage_api_key")] +) +async def test_hosted_missing_credentials_do_not_use_openai(endpoint, field, monkeypatch): + """Ensures an absent native key never leaks an ambient OpenAI key to another service.""" + monkeypatch.setattr(ENV_CONFIG, field, SecretStr("")) + monkeypatch.setenv("OPENAI_API_KEY", "must-not-leak") + + def respond(request): + pytest.fail("No request may be made without the selected provider credential.") + + async with Molto(httpx.AsyncClient(transport=httpx.MockTransport(respond))) as molto: + with pytest.raises(AuthenticationError): + await molto.embeddings( + {"model": "model", "input": "hello"}, + endpoint=endpoint, + options=CohereEmbeddingOptions(input_type="search_query") + if endpoint == "cohere/embed" + else None, + ) + + +@pytest.mark.parametrize("endpoint", ["cohere/embed", "voyage/embeddings"]) +@pytest.mark.parametrize("change", [{"input": [1, 2]}, {"input": [[1], [2]]}, {"user": "user"}]) +async def test_unsupported_translation_is_local(endpoint, change): + """Ensures token IDs and OpenAI user metadata cannot silently disappear in native translation.""" + + def respond(request): + pytest.fail("Unsupported translation must be rejected before HTTP.") + + async with Molto(httpx.AsyncClient(transport=httpx.MockTransport(respond))) as molto: + with pytest.raises(UnsupportedInput): + await molto.embeddings( + {"model": "model", "input": "hello", **change}, + endpoint=endpoint, + api_key="test-key", + options=CohereEmbeddingOptions(input_type="search_query") + if endpoint == "cohere/embed" + else None, + ) + + +@pytest.mark.parametrize( + "endpoint,options", + [ + ("cohere/embed", None), + ("cohere/embed", VoyageEmbeddingOptions()), + ("voyage/embeddings", CohereEmbeddingOptions(input_type="search_query")), + ], +) +async def test_native_task_options_are_not_interchangeable(endpoint, options): + """Ensures Cohere requires intent and options from another protocol cannot be silently reused.""" + async with Molto( + httpx.AsyncClient(transport=httpx.MockTransport(lambda _: pytest.fail("HTTP"))) + ) as molto: + with pytest.raises(UnsupportedInput): + await molto.embeddings( + {"model": "model", "input": "hello"}, + endpoint=endpoint, + api_key="test-key", + options=options, + ) + + +@pytest.mark.parametrize("provider", ["cohere", "voyageai"]) +async def test_native_extra_body_overrides_are_applied_before_output_projection(provider): + """Ensures upstream overrides control model, inputs, dimensions, encoding, and task together.""" + cohere = provider == "cohere" + fixture = ( + _recording(provider, "batch-base64") + if cohere + else yaml.safe_load( + (RECORDED.parent / "synthetic" / provider / "document-base64.yaml").read_text() + ) + ) + wire = fixture["request"]["body"] + + def respond(request): + assert json.loads(request.content) == wire + return httpx.Response(200, json=fixture["response"]["body"]) + + async with Molto(httpx.AsyncClient(transport=httpx.MockTransport(respond))) as molto: + result = await molto.embeddings( + { + "model": "overridden", + "input": "overridden", + "dimensions": 1, + "encoding_format": "float", + }, + endpoint="cohere/embed" if cohere else "voyage/embeddings", + api_key="test-key", + options=CohereEmbeddingOptions(input_type="search_query") + if cohere + else VoyageEmbeddingOptions(input_type="query"), + extra_body=wire, + ) + assert result.model == wire["model"] + assert len(result.data) == 2 + assert all(isinstance(item.embedding, str) for item in result.data) + + +@pytest.mark.parametrize("corruption", ["missing", "nested", "dimensions", "multiple"]) +async def test_cohere_malformed_output_rejects_without_coercion(corruption): + """Ensures incomplete, multi-vector, wrong-sized, or quantized responses cannot look valid.""" + fixture = _recording("cohere", "batch-document") + native = fixture["response"]["body"] + if corruption == "missing": + native["embeddings"]["float"].pop() + elif corruption == "nested": + native["embeddings"]["float"][0] = [[0.5], [0.25]] + elif corruption == "dimensions": + native["embeddings"]["float"][0].pop() + else: + native["embeddings"]["int8"] = [[1, 2], [3, 4]] + wire = fixture["request"]["body"] + async with Molto( + httpx.AsyncClient( + transport=httpx.MockTransport(lambda _: httpx.Response(200, json=native)) + ) + ) as molto: + with pytest.raises(ValueError): + await molto.embeddings( + { + "model": wire["model"], + "input": wire["texts"], + "dimensions": wire["output_dimension"], + }, + endpoint="cohere/embed", + api_key="test-key", + options=CohereEmbeddingOptions(input_type=wire["input_type"]), + ) + + +async def test_cohere_preserves_nested_embedding_metadata(): + """Ensures unfamiliar metadata alongside embeddings is retained without accepting another dtype.""" + fixture = _recording("cohere", "single-query") + native = fixture["response"]["body"] + native["embeddings"]["provider_revision"] = "synthetic-extension" + native["embeddings"]["int8"] = None + wire = fixture["request"]["body"] + async with Molto( + httpx.AsyncClient( + transport=httpx.MockTransport(lambda _: httpx.Response(200, json=native)) + ) + ) as molto: + result = await molto.embeddings( + {"model": wire["model"], "input": wire["texts"]}, + endpoint="cohere/embed", + api_key="test-key", + options=CohereEmbeddingOptions(input_type=wire["input_type"]), + ) + assert result.provider_metadata["embeddings"] == { + "provider_revision": "synthetic-extension", + "int8": None, + } + + +@pytest.mark.parametrize("indices", [[0, 0], [1, 2], [False, 1], ["0", 1]]) +async def test_voyage_invalid_indices_cannot_misassociate_inputs(indices): + """Ensures duplicate, missing, or non-integer native indices never associate vectors incorrectly.""" + native = {"data": [{"index": index, "embedding": [0.5]} for index in indices]} + async with Molto( + httpx.AsyncClient( + transport=httpx.MockTransport(lambda _: httpx.Response(200, json=native)) + ) + ) as molto: + with pytest.raises(ValueError): + await molto.embeddings( + {"model": "voyage-3.5-lite", "input": ["first", "second"]}, + endpoint="voyage/embeddings", + api_key="test-key", + ) + + +@pytest.mark.parametrize("endpoint", ["cohere/embed", "voyage/embeddings"]) +@pytest.mark.parametrize( + "override", + [ + {"output_dtype": "int8"}, + {"embedding_types": ["float", "binary"]}, + {"inputs": [{"content": [{"type": "text", "text": "grouped"}]}]}, + {"images": ["data:image/png;base64,AAAA"]}, + {"stream": True}, + ], +) +async def test_unsupported_native_overrides_fail_before_http(endpoint, override): + """Ensures raw transport options cannot turn dense text output into an unsupported mode.""" + async with Molto( + httpx.AsyncClient(transport=httpx.MockTransport(lambda _: pytest.fail("HTTP"))) + ) as molto: + with pytest.raises(UnsupportedInput): + await molto.embeddings( + {"model": "model", "input": "text"}, + endpoint=endpoint, + api_key="test-key", + options=CohereEmbeddingOptions(input_type="search_query") + if endpoint == "cohere/embed" + else None, + extra_body=override, + )